implement tag aliases, replacements, warnings
warnings are printed by `list-tags`
This commit is contained in:
parent
cae6400ec9
commit
67f0c3ded7
9 changed files with 319 additions and 118 deletions
7
Makefile
7
Makefile
|
@ -5,6 +5,7 @@ TMPDIR := _tmp
|
||||||
BUILDDIR := _build
|
BUILDDIR := _build
|
||||||
INFONAME := info.yaml
|
INFONAME := info.yaml
|
||||||
ROOT := https://gallery.niss.website
|
ROOT := https://gallery.niss.website
|
||||||
|
INDEX := $(DATADIR)/index.yaml
|
||||||
|
|
||||||
# SMALL = thumbnails, MED = single pages (link to full size)
|
# SMALL = thumbnails, MED = single pages (link to full size)
|
||||||
SMALL := 200
|
SMALL := 200
|
||||||
|
@ -28,7 +29,7 @@ BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS))
|
||||||
all: build
|
all: build
|
||||||
build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
|
build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
|
||||||
|
|
||||||
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
|
$(BUILDDIR)/index.html: $(INDEX) $(MAKEPAGES)
|
||||||
echo "[index] "$@
|
echo "[index] "$@
|
||||||
mkdir -p $(dir $@)
|
mkdir -p $(dir $@)
|
||||||
$(MAKEPAGES) $(MPFLAGS) index --root $(ROOT) $< --output $@
|
$(MAKEPAGES) $(MPFLAGS) index --root $(ROOT) $< --output $@
|
||||||
|
@ -85,7 +86,7 @@ $(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
|
||||||
-exec cp {} $@ \;
|
-exec cp {} $@ \;
|
||||||
|
|
||||||
|
|
||||||
$(TMPDIR)/index.mk: $(DATADIR)/index.yaml $(YAMLS) $(MAKEPAGES)
|
$(TMPDIR)/index.mk: $(INDEX) $(YAMLS) $(MAKEPAGES)
|
||||||
$(call depend-gallery)
|
$(call depend-gallery)
|
||||||
|
|
||||||
NODEPS := mostlyclean clean distclean list-tags
|
NODEPS := mostlyclean clean distclean list-tags
|
||||||
|
@ -109,7 +110,7 @@ distclean: clean
|
||||||
|
|
||||||
.PHONY: list-tags
|
.PHONY: list-tags
|
||||||
list-tags: $(MAKEPAGES)
|
list-tags: $(MAKEPAGES)
|
||||||
$(MAKEPAGES) list-tags --untagged --nsfw
|
$(MAKEPAGES) list-tags --untagged --warnings --nsfw --index $(INDEX)
|
||||||
|
|
||||||
|
|
||||||
HOST ?= gallery.niss.website
|
HOST ?= gallery.niss.website
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module GalleryPage (make) where
|
module GalleryPage (make) where
|
||||||
|
|
||||||
import BuilderQQ
|
import BuilderQQ
|
||||||
import Date
|
import Date
|
||||||
import Info
|
import Info
|
||||||
import qualified NsfwWarning
|
import qualified NsfwWarning
|
||||||
|
import TagTransforms
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.HashSet (HashSet)
|
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
import Data.List (intersperse, sort, sortOn)
|
import Data.List (intersperse, sort, sortOn)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
@ -17,12 +18,15 @@ import Data.Ord (Down (..))
|
||||||
import qualified Data.Text.Lazy as Lazy
|
import qualified Data.Text.Lazy as Lazy
|
||||||
import System.FilePath (takeDirectory, joinPath, splitPath)
|
import System.FilePath (takeDirectory, joinPath, splitPath)
|
||||||
|
|
||||||
make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
|
make :: Text -> IndexInfo -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
|
||||||
make root ginfo infos = toLazyText $ make' root ginfo infos
|
make root iinfo ginfo infos =
|
||||||
|
toLazyText $ make' root ginfo $
|
||||||
|
map (fmap $ transformInfoTags iinfo.tags) infos
|
||||||
|
|
||||||
|
|
||||||
make' :: Text -> GalleryInfo -> [(FilePath, Info)] -> Builder
|
make' :: Text -> GalleryInfo -> [(FilePath, Info)] -> Builder
|
||||||
make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|
|
make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos =
|
||||||
|
[b|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang=en>
|
<html lang=en>
|
||||||
<meta charset=utf-8>
|
<meta charset=utf-8>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{-# OPTIONS_GHC -fdefer-typed-holes #-}
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
module Info
|
module Info
|
||||||
(Info (..),
|
(Info (..),
|
||||||
anySfw, anyNsfw, allSfw, allNsfw,
|
anySfw, anyNsfw, allSfw, allNsfw,
|
||||||
|
@ -20,18 +19,23 @@ module Info
|
||||||
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
|
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
|
||||||
readArtistFilter, matchArtist, readNsfwFilter, matchNsfw, matchFilters,
|
readArtistFilter, matchArtist, readNsfwFilter, matchNsfw, matchFilters,
|
||||||
|
|
||||||
|
TagTransforms (..), emptyTransforms,
|
||||||
|
Implies (..), Replacement (..), Warning (..),
|
||||||
|
|
||||||
IndexInfo (..),
|
IndexInfo (..),
|
||||||
|
|
||||||
NoThumb (..), getThumb, thumbFile, pageFile, bigFile,
|
NoThumb (..), getThumb, thumbFile, pageFile, bigFile,
|
||||||
-- ** Reexports
|
-- ** Reexports
|
||||||
Date (..), Day (..), Text, NonEmpty (..))
|
Date (..), Day (..), Text, NonEmpty (..), Regex, HashSet, HashMap)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Date
|
import Date
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Bitraversable (bitraverse)
|
import Data.Bitraversable (bitraverse)
|
||||||
|
import Data.Char qualified as Char
|
||||||
import Data.Foldable (find)
|
import Data.Foldable (find)
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
import Data.HashMap.Strict qualified as HashMap
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
@ -53,6 +57,7 @@ import Data.YAML (FromYAML (..), (.:), (.:?), (.!=))
|
||||||
import Data.YAML qualified as YAML
|
import Data.YAML qualified as YAML
|
||||||
import GHC.Records
|
import GHC.Records
|
||||||
import System.FilePath ((</>), takeBaseName, takeExtension, splitExtension)
|
import System.FilePath ((</>), takeBaseName, takeExtension, splitExtension)
|
||||||
|
import Text.ParserCombinators.ReadP qualified as ReadP
|
||||||
import Text.Regex.TDFA (Regex)
|
import Text.Regex.TDFA (Regex)
|
||||||
import Text.Regex.TDFA qualified as Regex
|
import Text.Regex.TDFA qualified as Regex
|
||||||
|
|
||||||
|
@ -75,6 +80,7 @@ data Info =
|
||||||
nsfwOnly :: !Bool,
|
nsfwOnly :: !Bool,
|
||||||
tags :: !(HashSet Text),
|
tags :: !(HashSet Text),
|
||||||
nsfwTags :: !(HashSet Text),
|
nsfwTags :: !(HashSet Text),
|
||||||
|
excludeTags :: !(HashSet Text), -- remove tags that would be implied
|
||||||
desc :: !Desc,
|
desc :: !Desc,
|
||||||
nsfwDesc :: !Desc,
|
nsfwDesc :: !Desc,
|
||||||
bg :: !Bg,
|
bg :: !Bg,
|
||||||
|
@ -315,8 +321,8 @@ instance FromYAML Info where
|
||||||
parseYAML = YAML.withMap "info" \m -> do
|
parseYAML = YAML.withMap "info" \m -> do
|
||||||
checkKeys m ["date", "sort", "updates", "show-updated", "unlisted",
|
checkKeys m ["date", "sort", "updates", "show-updated", "unlisted",
|
||||||
"gallery-title", "title", "artist", "nsfw-only", "tags",
|
"gallery-title", "title", "artist", "nsfw-only", "tags",
|
||||||
"nsfw-tags", "desc", "nsfw-desc", "bg", "images", "thumb",
|
"nsfw-tags", "exclude-tags", "desc", "nsfw-desc", "bg",
|
||||||
"links", "extras"]
|
"images", "thumb", "links", "extras"]
|
||||||
Info <$> m .: "date"
|
Info <$> m .: "date"
|
||||||
<*> m .:? "sort" .!= ""
|
<*> m .:? "sort" .!= ""
|
||||||
<*> (m .:? "updates" >>= updateList)
|
<*> (m .:? "updates" >>= updateList)
|
||||||
|
@ -328,6 +334,7 @@ instance FromYAML Info where
|
||||||
<*> m .:? "nsfw-only" .!= False
|
<*> m .:? "nsfw-only" .!= False
|
||||||
<*> m .:? "tags" .!= []
|
<*> m .:? "tags" .!= []
|
||||||
<*> m .:? "nsfw-tags" .!= []
|
<*> m .:? "nsfw-tags" .!= []
|
||||||
|
<*> m .:? "exclude-tags" .!= []
|
||||||
<*> m .:? "desc" .!= NoDesc
|
<*> m .:? "desc" .!= NoDesc
|
||||||
<*> m .:? "nsfw-desc" .!= NoDesc
|
<*> m .:? "nsfw-desc" .!= NoDesc
|
||||||
<*> m .:? "bg" .!= Default
|
<*> m .:? "bg" .!= Default
|
||||||
|
@ -517,6 +524,101 @@ instance FromYAML ArtistFilter where
|
||||||
parseYAML = YAML.withStr "artist filter" readArtistFilter
|
parseYAML = YAML.withStr "artist filter" readArtistFilter
|
||||||
|
|
||||||
|
|
||||||
|
data TagTransforms =
|
||||||
|
TagTransforms {
|
||||||
|
implies :: ![Implies],
|
||||||
|
replace :: !(HashMap Text Text),
|
||||||
|
replaceWarn :: !(HashMap Text Text),
|
||||||
|
warn :: ![Warning]
|
||||||
|
} deriving Show
|
||||||
|
|
||||||
|
instance FromYAML TagTransforms where
|
||||||
|
parseYAML = YAML.withMap "tag transforms" \m -> do
|
||||||
|
checkKeys m ["implies", "replace", "replace-warn", "warn"]
|
||||||
|
TagTransforms <$> m .:? "implies" .!= []
|
||||||
|
<*> m .:? "replace" .!= []
|
||||||
|
<*> m .:? "replace-warn" .!= []
|
||||||
|
<*> m .:? "warn" .!= []
|
||||||
|
|
||||||
|
emptyTransforms :: TagTransforms
|
||||||
|
emptyTransforms = TagTransforms [] [] [] []
|
||||||
|
|
||||||
|
|
||||||
|
data SlashedRegex = SR Text Regex
|
||||||
|
|
||||||
|
regex :: MonadFail m => Text -> m Regex
|
||||||
|
regex body =
|
||||||
|
let comp = Regex.defaultCompOpt {Regex.caseSensitive = False}
|
||||||
|
exec = Regex.defaultExecOpt in
|
||||||
|
Regex.makeRegexOptsM comp exec body
|
||||||
|
|
||||||
|
withSR :: (Text -> Regex -> a) -> SlashedRegex -> a
|
||||||
|
withSR f (SR t r) = f t r
|
||||||
|
|
||||||
|
instance FromYAML SlashedRegex where
|
||||||
|
parseYAML = YAML.withStr "string or regex" \str -> do
|
||||||
|
guard $ Text.length str > 2 &&
|
||||||
|
Text.head str == '/' &&
|
||||||
|
Text.last str == '/'
|
||||||
|
let body = Text.drop 1 $ Text.dropEnd 1 str
|
||||||
|
SR body <$> regex body
|
||||||
|
|
||||||
|
|
||||||
|
data Implies = RegexI Text Regex [Replacement] | LiteralI Text [Text]
|
||||||
|
|
||||||
|
instance Show Implies where
|
||||||
|
showsPrec d (RegexI s _ rhss) =
|
||||||
|
showParen (d > 10) $
|
||||||
|
showString "RegexI " . showsPrec 11 s . showString " _ " .
|
||||||
|
showsPrec 10 rhss
|
||||||
|
showsPrec d (LiteralI s rhss) =
|
||||||
|
showParen (d > 10) $
|
||||||
|
showString "LiteralI " . showsPrec 11 s . showChar ' ' .
|
||||||
|
showsPrec 10 rhss
|
||||||
|
|
||||||
|
instance FromYAML Implies where
|
||||||
|
parseYAML = withPairM \k v ->
|
||||||
|
withSR RegexI <$> parseYAML k <*> parseYAML v
|
||||||
|
<|> LiteralI <$> parseYAML k <*> parseYAML v
|
||||||
|
|
||||||
|
|
||||||
|
data Warning = RegexW Text Regex | LiteralW Text
|
||||||
|
|
||||||
|
instance Show Warning where
|
||||||
|
showsPrec d (RegexW s _) =
|
||||||
|
showParen (d > 10) $
|
||||||
|
showString "RegexI " . showsPrec 11 s . showString " _"
|
||||||
|
showsPrec d (LiteralW s) =
|
||||||
|
showParen (d > 10) $
|
||||||
|
showString "LiteralI " . showsPrec 11 s
|
||||||
|
|
||||||
|
instance FromYAML Warning where
|
||||||
|
parseYAML y = withSR RegexW <$> parseYAML y
|
||||||
|
<|> LiteralW <$> parseYAML y
|
||||||
|
|
||||||
|
|
||||||
|
data Replacement = Re Text [Either String Int] deriving Show
|
||||||
|
|
||||||
|
parseReplacement :: MonadFail m => Text -> m Replacement
|
||||||
|
parseReplacement txt =
|
||||||
|
case ReadP.readP_to_S parser (Text.unpack txt) of
|
||||||
|
[(res, "")] -> pure $ Re txt res
|
||||||
|
_ -> fail $
|
||||||
|
"parse error in replacement string \"" <> Text.unpack txt <> "\""
|
||||||
|
where
|
||||||
|
parser = many (Left . pure <$> esc
|
||||||
|
<|> Left <$> lit
|
||||||
|
<|> Right . read <$> var) <* ReadP.eof
|
||||||
|
esc = ReadP.char '\\' *> ReadP.get
|
||||||
|
lit = ReadP.munch1 \c -> c /= '$' && c /= '\\'
|
||||||
|
var = bareVar <|> braceVar where
|
||||||
|
bareVar = ReadP.char '$' *> ReadP.munch1 Char.isDigit
|
||||||
|
braceVar = ReadP.string "${" *> ReadP.munch1 Char.isDigit <* ReadP.char '}'
|
||||||
|
|
||||||
|
instance FromYAML Replacement where
|
||||||
|
parseYAML = YAML.withStr "regex replacement string" parseReplacement
|
||||||
|
|
||||||
|
|
||||||
data IndexInfo =
|
data IndexInfo =
|
||||||
IndexInfo {
|
IndexInfo {
|
||||||
title :: !Text,
|
title :: !Text,
|
||||||
|
@ -538,54 +640,6 @@ instance FromYAML IndexInfo where
|
||||||
<*> m .:? "footer"
|
<*> m .:? "footer"
|
||||||
<*> m .:? "tags" .!= emptyTransforms
|
<*> m .:? "tags" .!= emptyTransforms
|
||||||
|
|
||||||
data TagTransforms =
|
|
||||||
TagTransforms {
|
|
||||||
implies :: !(HashMap ImpliesKey [Text]),
|
|
||||||
replace :: !(HashMap Text Text),
|
|
||||||
replaceWarn :: !(HashMap Text Text),
|
|
||||||
warn :: !(HashSet Text)
|
|
||||||
}
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data ImpliesKey = RegexIK Text Regex | LiteralIK Text
|
|
||||||
|
|
||||||
instance Eq ImpliesKey where
|
|
||||||
RegexIK s _ == RegexIK t _ = s == t
|
|
||||||
LiteralIK s == LiteralIK t = s == t
|
|
||||||
_ == _ = False
|
|
||||||
|
|
||||||
instance Show ImpliesKey where
|
|
||||||
showsPrec d (RegexIK s _) =
|
|
||||||
showParen (d > 10) $
|
|
||||||
showString "RegexIK " . showsPrec 11 s . showString " _"
|
|
||||||
showsPrec d (LiteralIK s) =
|
|
||||||
showParen (d > 10) $ showString "LiteralIK " . showsPrec 11 s
|
|
||||||
|
|
||||||
instance Hashable ImpliesKey where
|
|
||||||
hashWithSalt s (RegexIK str _) = hashWithSalt s ('R', str)
|
|
||||||
hashWithSalt s (LiteralIK str) = hashWithSalt s ('L', str)
|
|
||||||
|
|
||||||
emptyTransforms :: TagTransforms
|
|
||||||
emptyTransforms = TagTransforms [] [] [] []
|
|
||||||
|
|
||||||
instance FromYAML TagTransforms where
|
|
||||||
parseYAML = YAML.withMap "tag transforms" \m -> do
|
|
||||||
checkKeys m ["implies", "replace", "replace-warn", "warn"]
|
|
||||||
TagTransforms <$> m .:? "implies" .!= []
|
|
||||||
<*> m .:? "replace" .!= []
|
|
||||||
<*> m .:? "replace-warn" .!= []
|
|
||||||
<*> m .:? "warn" .!= []
|
|
||||||
|
|
||||||
instance FromYAML ImpliesKey where
|
|
||||||
parseYAML = YAML.withStr "string or regex" \str -> pure
|
|
||||||
if Text.length str > 2 &&
|
|
||||||
Text.head str == '/' &&
|
|
||||||
Text.last str == '/'
|
|
||||||
then
|
|
||||||
let body = Text.drop 1 $ Text.dropEnd 1 str in
|
|
||||||
RegexIK body (Regex.makeRegex body)
|
|
||||||
else LiteralIK str
|
|
||||||
|
|
||||||
|
|
||||||
data Pair a b = Pair !a !b
|
data Pair a b = Pair !a !b
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,11 @@ module ListTags where
|
||||||
|
|
||||||
import Info
|
import Info
|
||||||
import Options (TagSort (..))
|
import Options (TagSort (..))
|
||||||
|
import TagTransforms
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
import Data.HashMap.Strict (HashMap)
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.HashSet (HashSet)
|
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
import Data.List (sort, sortBy)
|
import Data.List (sort, sortBy)
|
||||||
import Data.Ord (comparing)
|
import Data.Ord (comparing)
|
||||||
|
@ -17,44 +16,62 @@ import Text.Printf (printf)
|
||||||
|
|
||||||
type TagStats = HashMap Text Int
|
type TagStats = HashMap Text Int
|
||||||
type PathSet = HashSet FilePath
|
type PathSet = HashSet FilePath
|
||||||
|
type FileWarnings = [(FilePath, TagWarning)]
|
||||||
|
|
||||||
data Stats =
|
data Stats =
|
||||||
Stats {
|
Stats {
|
||||||
tags :: TagStats,
|
tags :: TagStats,
|
||||||
untagged :: PathSet
|
untagged :: PathSet,
|
||||||
|
warns :: FileWarnings
|
||||||
}
|
}
|
||||||
|
|
||||||
instance Semigroup Stats where
|
instance Semigroup Stats where
|
||||||
Stats {tags = t1, untagged = u1} <> Stats {tags = t2, untagged = u2} =
|
s1 <> s2 = Stats {
|
||||||
Stats {tags = HashMap.unionWith (+) t1 t2, untagged = u1 <> u2}
|
tags = HashMap.unionWith (+) s1.tags s2.tags,
|
||||||
|
untagged = s1.untagged <> s2.untagged,
|
||||||
|
warns = s1.warns <> s2.warns
|
||||||
|
}
|
||||||
|
|
||||||
instance Monoid Stats where
|
instance Monoid Stats where
|
||||||
mempty = Stats {tags = [], untagged = []}
|
mempty = Stats {tags = [], untagged = [], warns = []}
|
||||||
|
|
||||||
|
|
||||||
stats1 :: Bool -> FilePath -> Info -> Stats
|
stats1 :: TagTransforms -> Bool -> FilePath -> Info -> Stats
|
||||||
stats1 nsfw path (Info {tags, nsfwTags}) =
|
stats1 tt nsfw path (Info {tags, nsfwTags}) =
|
||||||
let tags' = if nsfw then tags <> nsfwTags else tags in
|
let startTags = if nsfw then tags <> nsfwTags else tags
|
||||||
if null tags' then
|
TR outTags warns = applyTransforms tt startTags
|
||||||
Stats {tags = [], untagged = [path]}
|
in
|
||||||
|
if null outTags then
|
||||||
|
Stats {tags = [], untagged = [path], warns = map (path,) warns}
|
||||||
else
|
else
|
||||||
let tagMap = HashMap.fromList $ map (, 1) $ HashSet.toList tags' in
|
let tagMap = HashMap.fromList $ map (, 1) $ HashSet.toList outTags in
|
||||||
Stats {tags = tagMap, untagged = []}
|
Stats {tags = tagMap, untagged = [], warns = map (path,) warns}
|
||||||
|
|
||||||
stats :: Bool -> [(FilePath, Info)] -> Stats
|
stats :: IndexInfo -> Bool -> [(FilePath, Info)] -> Stats
|
||||||
stats nsfw = foldMap $ uncurry $ stats1 nsfw
|
stats iinfo nsfw = foldMap $ uncurry $ stats1 iinfo.tags nsfw
|
||||||
|
|
||||||
run :: Bool -> Bool -> TagSort -> [(FilePath, Info)] -> IO ()
|
run :: IndexInfo
|
||||||
run nsfw listUntagged ts infos = do
|
-> Bool -- ^ include nsfw?
|
||||||
let Stats {tags, untagged} = stats nsfw infos
|
-> Bool -- ^ list files with no tags
|
||||||
|
-> Bool -- ^ print tag warnings
|
||||||
|
-> TagSort
|
||||||
|
-> [(FilePath, Info)] ->
|
||||||
|
IO ()
|
||||||
|
run iinfo nsfw listUntagged showWarnings ts infos = do
|
||||||
|
let Stats {tags, untagged, warns} = stats iinfo nsfw infos
|
||||||
let sortedTags = sortBy (cmp ts) $ map swap $ HashMap.toList tags
|
let sortedTags = sortBy (cmp ts) $ map swap $ HashMap.toList tags
|
||||||
putStrLn "TAGS\n----"
|
putStrLn "TAGS\n----"
|
||||||
|
printf "%5d: [total]\n" (length infos)
|
||||||
for_ sortedTags \(count, path) ->
|
for_ sortedTags \(count, path) ->
|
||||||
printf "%4d: %s\n" count path
|
printf "%5d: %s\n" count path
|
||||||
when (listUntagged && not (null untagged)) $ do
|
when (listUntagged && not (null untagged)) $ do
|
||||||
let sortedUntagged = sort $ HashSet.toList untagged
|
let sortedUntagged = sort $ HashSet.toList untagged
|
||||||
putStrLn "\nUNTAGGED FILES\n--------------"
|
putStrLn "\nUNTAGGED FILES\n--------------"
|
||||||
for_ sortedUntagged $ printf " - %s\n"
|
for_ sortedUntagged $ printf " - %s\n"
|
||||||
|
when (showWarnings && not (null warns)) $ do
|
||||||
|
let hdr :: String = printf "WARNINGS (%d)" (length warns)
|
||||||
|
printf "\n%s\n%s\n" hdr (const '-' <$> hdr)
|
||||||
|
for_ warns \(file, warn) -> putStrLn $ showWarning file warn
|
||||||
|
|
||||||
cmp :: TagSort -> (Int, Text) -> (Int, Text) -> Ordering
|
cmp :: TagSort -> (Int, Text) -> (Int, Text) -> Ordering
|
||||||
cmp SortFreq = flip $ comparing fst
|
cmp SortFreq = flip $ comparing fst
|
||||||
|
|
|
@ -24,10 +24,12 @@ import qualified GalleryPage
|
||||||
import qualified IndexPage
|
import qualified IndexPage
|
||||||
import qualified RSS
|
import qualified RSS
|
||||||
import qualified ListTags
|
import qualified ListTags
|
||||||
|
import TagTransforms
|
||||||
|
|
||||||
#ifdef PRETTY_VERBOSE
|
#ifdef PRETTY_VERBOSE
|
||||||
import qualified Text.PrettyPrint as PP
|
import qualified Text.PrettyPrint as PP
|
||||||
import qualified Text.Show.Pretty as PP
|
import qualified Text.Show.Pretty as PP
|
||||||
|
import Data.Function ((&))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,19 +51,20 @@ main = do
|
||||||
|
|
||||||
main2 :: HasVerbose => ModeOptions -> IO ()
|
main2 :: HasVerbose => ModeOptions -> IO ()
|
||||||
main2 (SinglePage {root, file, prefix, index, dataDir, nsfw, output}) = do
|
main2 (SinglePage {root, file, prefix, index, dataDir, nsfw, output}) = do
|
||||||
siteName <- (.title) <$> readYAML @IndexInfo index
|
iinfo <- readYAML index
|
||||||
info <- readYAML file
|
info <- transformInfoTags iinfo.tags <$> readYAML file
|
||||||
printV $ "contents" := info
|
printV $ "contents" := info
|
||||||
let dir = takeDirectory $ makeRelative dataDir file
|
let dir = takeDirectory $ makeRelative dataDir file
|
||||||
page <- SinglePage.make root siteName prefix nsfw dataDir dir info
|
page <- SinglePage.make root iinfo prefix nsfw dataDir dir info
|
||||||
writeOutput output page
|
writeOutput output page
|
||||||
|
|
||||||
main2 (GalleryPage {root, files, prefix, index, output, dataDir}) = do
|
main2 (GalleryPage {root, files, prefix, index, output, dataDir}) = do
|
||||||
(_, ginfo) <- galleryFromIndex index prefix
|
(iinfo, _, ginfo) <- galleryFromIndex index prefix
|
||||||
printV $ "gallery_info" := ginfo
|
printV $ "gallery_info" := ginfo
|
||||||
infos <- mapM (infoYAML dataDir) files
|
infos <- traverse (infoYAML dataDir) files
|
||||||
|
& fmap (fmap $ fmap $ transformInfoTags iinfo.tags)
|
||||||
printV $ "infos" := infos
|
printV $ "infos" := infos
|
||||||
let page = GalleryPage.make root ginfo infos
|
let page = GalleryPage.make root iinfo ginfo infos
|
||||||
writeOutput output page
|
writeOutput output page
|
||||||
|
|
||||||
main2 (IndexPage {root, file, output}) = do
|
main2 (IndexPage {root, file, output}) = do
|
||||||
|
@ -71,9 +74,9 @@ main2 (IndexPage {root, file, output}) = do
|
||||||
writeOutput output page
|
writeOutput output page
|
||||||
|
|
||||||
main2 (RSS {files, root, index, prefix, output, dataDir}) = do
|
main2 (RSS {files, root, index, prefix, output, dataDir}) = do
|
||||||
(name, ginfo) <- galleryFromIndex index prefix
|
(_, name, ginfo) <- galleryFromIndex index prefix
|
||||||
printV $ "gallery_info" := ginfo
|
printV $ "gallery_info" := ginfo
|
||||||
infos <- mapM (infoYAML dataDir) files
|
infos <- traverse (infoYAML dataDir) files
|
||||||
printV $ "infos" := infos
|
printV $ "infos" := infos
|
||||||
let output' = takeFileName <$> output
|
let output' = takeFileName <$> output
|
||||||
let rss = RSS.make root name ginfo output' infos
|
let rss = RSS.make root name ginfo output' infos
|
||||||
|
@ -90,19 +93,24 @@ main2 (DependSingle {index, file, nsfw, output, prefix, buildDir, dataDir}) = do
|
||||||
main2 (DependGallery {file, output, buildDir, dataDir, tmpDir, infoName}) = do
|
main2 (DependGallery {file, output, buildDir, dataDir, tmpDir, infoName}) = do
|
||||||
IndexInfo {galleries} <- readYAML file
|
IndexInfo {galleries} <- readYAML file
|
||||||
printV $ "galleries" := galleries
|
printV $ "galleries" := galleries
|
||||||
infos <- mapM (infoYAML dataDir) =<< findInfos dataDir infoName
|
infos <- findInfos dataDir infoName
|
||||||
|
>>= traverse (infoYAML dataDir)
|
||||||
printV $ "info_files" := infos
|
printV $ "info_files" := infos
|
||||||
let dependGallery0 g = dependGallery' g file infos buildDir dataDir tmpDir
|
let dependGallery0 g = dependGallery' g file infos buildDir dataDir tmpDir
|
||||||
let deps = toLazyText $ mconcat $ intersperse "\n\n\n" $
|
let deps = toLazyText $ mconcat $ intersperse "\n\n\n" $
|
||||||
map dependGallery0 galleries
|
map dependGallery0 galleries
|
||||||
writeOutput output deps
|
writeOutput output deps
|
||||||
|
|
||||||
main2 (ListTags {nsfw, listUntagged, dataDir, infoName, sortBy}) = do
|
main2 (ListTags {nsfw, listUntagged, showWarnings, index,
|
||||||
infos <- mapM (infoYAML dataDir) =<< findInfos dataDir infoName
|
dataDir, infoName, sortBy}) = do
|
||||||
|
printV $ "index" := index
|
||||||
|
iinfo <- readYAML index
|
||||||
|
infos <- findInfos dataDir infoName >>= traverse (infoYAML dataDir)
|
||||||
printV $ "info_files" := infos
|
printV $ "info_files" := infos
|
||||||
ListTags.run nsfw listUntagged sortBy infos
|
ListTags.run iinfo nsfw listUntagged showWarnings sortBy infos
|
||||||
|
|
||||||
|
|
||||||
|
-- | applies tag transformations also
|
||||||
infoYAML :: FilePath -- ^ data dir
|
infoYAML :: FilePath -- ^ data dir
|
||||||
-> FilePath -- ^ yaml file
|
-> FilePath -- ^ yaml file
|
||||||
-> IO (FilePath, Info) -- relative filename, contents
|
-> IO (FilePath, Info) -- relative filename, contents
|
||||||
|
@ -118,10 +126,12 @@ findInfos dataDir infoName =
|
||||||
readYAML :: YAML.FromYAML a => FilePath -> IO a
|
readYAML :: YAML.FromYAML a => FilePath -> IO a
|
||||||
readYAML file = ByteString.readFile file >>= decode1Must file
|
readYAML file = ByteString.readFile file >>= decode1Must file
|
||||||
|
|
||||||
galleryFromIndex :: FilePath -> FilePath -> IO (Strict.Text, GalleryInfo)
|
galleryFromIndex :: FilePath -> FilePath ->
|
||||||
|
IO (IndexInfo, Strict.Text, GalleryInfo)
|
||||||
galleryFromIndex file prefix = do
|
galleryFromIndex file prefix = do
|
||||||
IndexInfo {title, galleries} <- readYAML file
|
iinfo@(IndexInfo {title, galleries}) <- readYAML file
|
||||||
maybe (fail $ "no gallery with prefix " ++ prefix) (pure . (title,)) $
|
maybe (fail $ "no gallery with prefix " ++ prefix)
|
||||||
|
(pure . (iinfo, title,)) $
|
||||||
List.find (\g -> g.prefix == prefix) galleries
|
List.find (\g -> g.prefix == prefix) galleries
|
||||||
|
|
||||||
decode1Must :: YAML.FromYAML a => FilePath -> ByteString -> IO a
|
decode1Must :: YAML.FromYAML a => FilePath -> ByteString -> IO a
|
||||||
|
|
|
@ -61,8 +61,10 @@ data ModeOptions =
|
||||||
| ListTags {
|
| ListTags {
|
||||||
dataDir :: FilePath,
|
dataDir :: FilePath,
|
||||||
infoName :: FilePath,
|
infoName :: FilePath,
|
||||||
|
index :: FilePath,
|
||||||
nsfw :: Bool,
|
nsfw :: Bool,
|
||||||
listUntagged :: Bool,
|
listUntagged :: Bool,
|
||||||
|
showWarnings :: Bool,
|
||||||
sortBy :: TagSort
|
sortBy :: TagSort
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving Show
|
||||||
|
@ -148,14 +150,17 @@ optionsParser = globalOpts `info` mainInfo where
|
||||||
dgInfo = progDesc "generate makefile dependencies for a gallery"
|
dgInfo = progDesc "generate makefile dependencies for a gallery"
|
||||||
|
|
||||||
listTags = command "list-tags" $ ltOpts `info` ltInfo
|
listTags = command "list-tags" $ ltOpts `info` ltInfo
|
||||||
ltOpts = ListTags <$> dataDir <*> infoName <*> nsfwT
|
ltOpts = ListTags <$> dataDir <*> infoName <*> indexFile <*> nsfwT
|
||||||
<*> listUntagged_ <*> listSort
|
<*> listUntagged <*> showWarnings <*> listSort
|
||||||
nsfwT = switch $
|
nsfwT = switch $
|
||||||
short 'n' <> long "nsfw" <>
|
short 'n' <> long "nsfw" <>
|
||||||
help "include nsfw tags"
|
help "include nsfw tags"
|
||||||
listUntagged_ = switch $
|
listUntagged = switch $
|
||||||
short 'U' <> long "untagged" <>
|
short 'U' <> long "untagged" <>
|
||||||
help "list files with no tags"
|
help "list files with no tags"
|
||||||
|
showWarnings = switch $
|
||||||
|
short 'W' <> long "warnings" <>
|
||||||
|
help "show tag warnings"
|
||||||
listSort = fmap toSort $ switch $
|
listSort = fmap toSort $ switch $
|
||||||
short 'a' <> long "alpha" <>
|
short 'a' <> long "alpha" <>
|
||||||
help "sort alphabetically instead of by frequency"
|
help "sort alphabetically instead of by frequency"
|
||||||
|
|
|
@ -4,21 +4,21 @@ module SinglePage (make) where
|
||||||
import Date
|
import Date
|
||||||
import Info
|
import Info
|
||||||
import BuilderQQ
|
import BuilderQQ
|
||||||
import qualified NsfwWarning
|
import NsfwWarning qualified
|
||||||
|
import TagTransforms
|
||||||
|
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Data.Char (isSpace)
|
||||||
|
import Data.Foldable
|
||||||
|
import Data.HashSet qualified as HashSet
|
||||||
import Data.List (sort, intersperse)
|
import Data.List (sort, intersperse)
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
import qualified Data.Text as Strict
|
|
||||||
import qualified Data.Text.Lazy as Lazy
|
|
||||||
import System.FilePath (joinPath, splitPath)
|
|
||||||
import Data.HashSet (HashSet)
|
|
||||||
import qualified Data.HashSet as HashSet
|
|
||||||
import Data.Traversable
|
|
||||||
import Data.Semigroup
|
import Data.Semigroup
|
||||||
import Data.List.NonEmpty (toList)
|
import Data.Text qualified as Strict
|
||||||
import Data.Char (isSpace)
|
import Data.Text.Lazy qualified as Lazy
|
||||||
|
import Data.Traversable
|
||||||
|
import System.FilePath (joinPath, splitPath)
|
||||||
|
|
||||||
|
|
||||||
-- | e.g. only nsfw images are present for a non-nsfw page
|
-- | e.g. only nsfw images are present for a non-nsfw page
|
||||||
|
@ -32,22 +32,25 @@ instance Show NoEligibleImages where
|
||||||
|
|
||||||
|
|
||||||
make :: Text -- ^ website root
|
make :: Text -- ^ website root
|
||||||
-> Text -- ^ website name
|
-> IndexInfo
|
||||||
-> FilePath -- ^ gallery prefix
|
-> FilePath -- ^ gallery prefix
|
||||||
-> Bool -- ^ nsfw?
|
-> Bool -- ^ nsfw?
|
||||||
-> FilePath -- ^ data dir
|
-> FilePath -- ^ data dir
|
||||||
-> FilePath -- ^ subdir of datadir containing this @info.yaml@
|
-> FilePath -- ^ subdir of datadir containing this @info.yaml@
|
||||||
-> Info -> IO Lazy.Text
|
-> Info -> IO Lazy.Text
|
||||||
make root siteName prefix nsfw dataDir dir info =
|
make root iinfo prefix nsfw dataDir dir info =
|
||||||
toLazyText <$> make' root siteName prefix nsfw dataDir dir info
|
fmap toLazyText $
|
||||||
|
make' root iinfo prefix nsfw dataDir dir $
|
||||||
|
transformInfoTags iinfo.tags info
|
||||||
|
|
||||||
make' :: Text -> Text -> FilePath -> Bool -> FilePath -> FilePath -> Info
|
make' :: Text -> IndexInfo -> FilePath -> Bool -> FilePath -> FilePath -> Info
|
||||||
-> IO Builder
|
-> IO Builder
|
||||||
make' root siteName prefix nsfw _dataDir dir
|
make' root iinfo prefix nsfw _dataDir dir
|
||||||
info@(Info {date, title, artist, bg}) = do
|
info@(Info {date, title, artist, bg}) = do
|
||||||
images <- maybe (throw $ NoEligibleImages title) pure $ imagesFor nsfw info
|
images <- maybe (throw $ NoEligibleImages title) pure $ imagesFor nsfw info
|
||||||
|
|
||||||
let undir = joinPath (replicate (length (splitPath dir)) "..")
|
let undir = joinPath (replicate (length (splitPath dir)) "..")
|
||||||
|
let siteName = iinfo.title
|
||||||
|
|
||||||
let formattedDate = formatLong date
|
let formattedDate = formatLong date
|
||||||
|
|
||||||
|
@ -59,7 +62,8 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
|
|
||||||
let artistSection = makeArtist artist
|
let artistSection = makeArtist artist
|
||||||
let descSection = makeDesc $ descFor nsfw info
|
let descSection = makeDesc $ descFor nsfw info
|
||||||
let tagsList = makeTags undir $ tagsFor nsfw info
|
let tags = tagsFor nsfw info
|
||||||
|
let tagsList = makeTags undir tags
|
||||||
let linksList = extLinks $ linksFor nsfw info
|
let linksList = extLinks $ linksFor nsfw info
|
||||||
let updates = sort $ updatesFor nsfw info
|
let updates = sort $ updatesFor nsfw info
|
||||||
let updatesList = makeUpdates updates
|
let updatesList = makeUpdates updates
|
||||||
|
|
103
make-pages/TagTransforms.hs
Normal file
103
make-pages/TagTransforms.hs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
module TagTransforms
|
||||||
|
(TransformResult (..),
|
||||||
|
TagWarning (..), showWarning, printWarning,
|
||||||
|
matchWarning, applyImplies, applyTransforms1, applyTransforms,
|
||||||
|
transformInfoTags)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Info
|
||||||
|
import BuilderQQ qualified as Builder
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Array (Array)
|
||||||
|
import Data.Array.Base ((!?))
|
||||||
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
import Data.HashSet qualified as HashSet
|
||||||
|
import Text.Regex.TDFA qualified as Regex
|
||||||
|
import System.IO (hPutStrLn, stderr)
|
||||||
|
import Control.Applicative (Alternative(..), asum)
|
||||||
|
import Control.Monad.Writer
|
||||||
|
|
||||||
|
|
||||||
|
data TagWarning
|
||||||
|
= ReplaceWarn Text Text
|
||||||
|
| PresenceWarn Text
|
||||||
|
| UndefinedCapture Int Text
|
||||||
|
| ReplaceParseError Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data TransformResult = TR (HashSet Text) [TagWarning]
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance Semigroup TransformResult where
|
||||||
|
TR t1 w1 <> TR t2 w2 = TR (t1 <> t2) (w1 <> w2)
|
||||||
|
instance Monoid TransformResult where
|
||||||
|
mempty = TR [] []
|
||||||
|
|
||||||
|
showWarning :: FilePath -> TagWarning -> String
|
||||||
|
showWarning file (ReplaceWarn from to) =
|
||||||
|
file ++ ": contains tag " ++ show from ++
|
||||||
|
" (replacing with " ++ show to ++ ")"
|
||||||
|
showWarning file (PresenceWarn what) =
|
||||||
|
file ++ ": contains tag " ++ show what
|
||||||
|
showWarning file (UndefinedCapture i txt) =
|
||||||
|
file ++ ": replacement string " ++ show txt ++
|
||||||
|
" contains undefined capture group $" ++ show i
|
||||||
|
showWarning file (ReplaceParseError txt) =
|
||||||
|
file ++ ": cannot parse replacement string " ++ show txt
|
||||||
|
|
||||||
|
printWarning :: FilePath -> TagWarning -> IO ()
|
||||||
|
printWarning f w =
|
||||||
|
hPutStrLn stderr $ "[WARN] " ++ showWarning f w
|
||||||
|
|
||||||
|
matchWarning :: Text -> Warning -> Bool
|
||||||
|
matchWarning txt (RegexW _ rx) = Regex.match rx txt
|
||||||
|
matchWarning txt (LiteralW l) = l == txt
|
||||||
|
|
||||||
|
applyImplies1 :: Array Int Text -> Replacement -> TransformResult
|
||||||
|
applyImplies1 subs (Re txt rhs) =
|
||||||
|
case traverse app rhs of
|
||||||
|
Left i -> TR [] [UndefinedCapture i txt]
|
||||||
|
Right oks -> TR [Builder.toStrictText $ mconcat oks] []
|
||||||
|
where
|
||||||
|
app (Left str) = Right $ Builder.build str
|
||||||
|
app (Right i) = case subs !? i of
|
||||||
|
Just sub -> Right $ Builder.build sub
|
||||||
|
Nothing -> Left i
|
||||||
|
|
||||||
|
applyImplies :: Text -> Implies -> Maybe TransformResult
|
||||||
|
applyImplies tag (RegexI _ rx outs) =
|
||||||
|
let res = Regex.mrSubs $ Regex.match rx tag in
|
||||||
|
if null res then empty else pure $ foldMap (applyImplies1 res) outs
|
||||||
|
applyImplies tag (LiteralI str outs) =
|
||||||
|
TR (HashSet.fromList outs) [] <$ guard (tag == str)
|
||||||
|
|
||||||
|
ifJust :: Applicative m => Maybe a -> (a -> m ()) -> m Bool
|
||||||
|
ifJust m f = maybe (pure False) (\x -> True <$ f x) m
|
||||||
|
|
||||||
|
applyTransforms1 :: TagTransforms -> Text -> TransformResult
|
||||||
|
applyTransforms1 t = execWriter . go where
|
||||||
|
go tag = do
|
||||||
|
replacedW <- ifJust (HashMap.lookup tag t.replaceWarn) \out -> do
|
||||||
|
tell $ TR [] [ReplaceWarn tag out]
|
||||||
|
go out
|
||||||
|
replaced <- ifJust (HashMap.lookup tag t.replace) go
|
||||||
|
unless (replacedW || replaced) do
|
||||||
|
tell $ TR [tag] []
|
||||||
|
when (any (matchWarning tag) t.warn) do
|
||||||
|
tell $ TR [] [PresenceWarn tag]
|
||||||
|
forM_ (asum $ map (applyImplies tag) t.implies) \(TR tags ws) -> do
|
||||||
|
tell $ TR [] ws
|
||||||
|
forM_ tags go
|
||||||
|
|
||||||
|
applyTransforms :: Foldable f => TagTransforms -> f Text -> TransformResult
|
||||||
|
applyTransforms t tags = foldMap (applyTransforms1 t) tags
|
||||||
|
|
||||||
|
transformInfoTags :: TagTransforms -> Info -> Info
|
||||||
|
transformInfoTags t (Info {..}) =
|
||||||
|
let TR tags' _ = applyTransforms t tags
|
||||||
|
TR nsfwTags' _ = applyTransforms t nsfwTags in
|
||||||
|
Info {
|
||||||
|
tags = HashSet.difference tags' excludeTags,
|
||||||
|
nsfwTags = HashSet.difference nsfwTags' excludeTags, ..
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ executable make-pages
|
||||||
BuilderQQ,
|
BuilderQQ,
|
||||||
Date,
|
Date,
|
||||||
Info,
|
Info,
|
||||||
|
TagTransforms,
|
||||||
Depend,
|
Depend,
|
||||||
NsfwWarning,
|
NsfwWarning,
|
||||||
GalleryPage,
|
GalleryPage,
|
||||||
|
@ -44,12 +45,14 @@ executable make-pages
|
||||||
TemplateHaskell
|
TemplateHaskell
|
||||||
build-depends:
|
build-depends:
|
||||||
base >= 4.16.4 && < 4.21,
|
base >= 4.16.4 && < 4.21,
|
||||||
|
array == 0.5.7.*,
|
||||||
bytestring >= 0.11.3.1 && < 0.14,
|
bytestring >= 0.11.3.1 && < 0.14,
|
||||||
containers >= 0.6.0.1 && < 0.8,
|
containers >= 0.6.0.1 && < 0.8,
|
||||||
filemanip ^>= 0.3.6.3,
|
filemanip ^>= 0.3.6.3,
|
||||||
filepath >= 1.4.2.1 && < 1.6,
|
filepath >= 1.4.2.1 && < 1.6,
|
||||||
hashable >= 1.3.0.0 && < 1.5,
|
hashable >= 1.3.0.0 && < 1.5,
|
||||||
HsYAML ^>= 0.2.1.0,
|
HsYAML ^>= 0.2.1.0,
|
||||||
|
mtl == 2.3.1.*,
|
||||||
optparse-applicative ^>= 0.15.1.0,
|
optparse-applicative ^>= 0.15.1.0,
|
||||||
process ^>= 1.6.8.2,
|
process ^>= 1.6.8.2,
|
||||||
regex-tdfa == 1.3.2.*,
|
regex-tdfa == 1.3.2.*,
|
||||||
|
|
Loading…
Reference in a new issue