first
This commit is contained in:
commit
77a53e06a5
21 changed files with 1070 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dist-newstyle
|
||||||
|
_build
|
||||||
|
_tmp
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "lang"]
|
||||||
|
path = lang
|
||||||
|
url = https://git.rhiannon.website/rhi/lang
|
112
Makefile
Normal file
112
Makefile
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
HOST ?= rhiannon.website
|
||||||
|
REMOTE_USER ?= www-data
|
||||||
|
IDFILE ?= ~/.ssh/xyz
|
||||||
|
REMOTE_DIR ?= blog
|
||||||
|
|
||||||
|
TMPDIR ?= _tmp
|
||||||
|
BUILDDIR ?= _build
|
||||||
|
POSTSDIR ?= posts
|
||||||
|
TEMPLATEDIR ?= templates
|
||||||
|
|
||||||
|
STATICEXTS := yaml
|
||||||
|
|
||||||
|
POSTS != find $(POSTSDIR) -name '*.md'
|
||||||
|
STATICS != parallel find $(POSTSDIR) -name '\*.{}' ::: $(STATICEXTS)
|
||||||
|
OUTPUTPOSTS = \
|
||||||
|
$(patsubst $(POSTSDIR)/%.md,$(BUILDDIR)/%.html,$(POSTS)) \
|
||||||
|
$(patsubst $(POSTSDIR)/%,$(BUILDDIR)/%,$(STATICS)) \
|
||||||
|
$(BUILDDIR)/all-posts.html
|
||||||
|
|
||||||
|
STYLE != find style -type f
|
||||||
|
OUTPUTSTYLE = $(patsubst %,$(BUILDDIR)/%,$(STYLE))
|
||||||
|
|
||||||
|
OUTPUT = $(OUTPUTPOSTS) $(OUTPUTSTYLE)
|
||||||
|
|
||||||
|
LANGFILTER = $(TMPDIR)/langfilter
|
||||||
|
LAANTAS_SCRIPT = $(TMPDIR)/laantas-script
|
||||||
|
ALL_TAGS = $(TMPDIR)/all-tags
|
||||||
|
POST_LISTS = $(TMPDIR)/post-lists
|
||||||
|
NICE_DATE = $(TMPDIR)/nice-date
|
||||||
|
EXECS = \
|
||||||
|
$(LANGFILTER) $(LAANTAS_SCRIPT) \
|
||||||
|
$(ALL_TAGS) $(POST_LISTS) $(NICE_DATE)
|
||||||
|
|
||||||
|
CABAL_FLAGS ?= -O -v0
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: build
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: $(EXECS) $(OUTPUT)
|
||||||
|
|
||||||
|
|
||||||
|
POSTDEPS = $(TEMPLATEDIR)/* $(LANGFILTER) $(LAANTAS_SCRIPT)
|
||||||
|
|
||||||
|
$(TMPDIR)/all-tags.md $(TMPDIR)/tags.mk &: $(POSTS) $(ALL_TAGS)
|
||||||
|
@echo "[all-tags]"
|
||||||
|
$(ALL_TAGS) $(POSTSDIR) $(TMPDIR)/all-tags.md $(TMPDIR)/tags.mk
|
||||||
|
build: $(BUILDDIR)/all-tags.html
|
||||||
|
include $(TMPDIR)/tags.mk
|
||||||
|
|
||||||
|
$(BUILDDIR)/%.html: $(POSTSDIR)/%.md $(POSTDEPS) ; $(call pandoc,post)
|
||||||
|
$(BUILDDIR)/%.html: $(TMPDIR)/%.md $(POSTDEPS) ; $(call pandoc,meta)
|
||||||
|
|
||||||
|
define pandoc
|
||||||
|
@echo "[pandoc] $<"
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
mkdir -p $(basename $@)
|
||||||
|
LAANTAS_SCRIPT="$(LAANTAS_SCRIPT)" \
|
||||||
|
DIRNAME="$(basename $@)" \
|
||||||
|
FILENAME="$@" \
|
||||||
|
pandoc -s --toc --template $(TEMPLATEDIR)/$(1).html -o $@ $< \
|
||||||
|
--filter $(LANGFILTER) --filter $(NICE_DATE) --mathjax
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
|
$(BUILDDIR)/%: $(POSTSDIR)/% ; $(copy)
|
||||||
|
$(BUILDDIR)/%: $(TMPDIR)/% ; $(copy)
|
||||||
|
$(BUILDDIR)/%: % ; $(copy)
|
||||||
|
|
||||||
|
define copy
|
||||||
|
@echo "[copy] $<"
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
cp $< $@
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
|
BLOG_META_DEPS != find blog-meta -type f
|
||||||
|
|
||||||
|
$(LANGFILTER): lang/langfilter/* ; $(call cabal-exe)
|
||||||
|
$(LAANTAS_SCRIPT): lang/laantas-script/* ; $(call cabal-exe)
|
||||||
|
$(ALL_TAGS): $(BLOG_META_DEPS) ; $(call cabal-exe,blog-meta:)
|
||||||
|
$(POST_LISTS): $(BLOG_META_DEPS) ; $(call cabal-exe,blog-meta:)
|
||||||
|
$(NICE_DATE): $(BLOG_META_DEPS) ; $(call cabal-exe,blog-meta:)
|
||||||
|
|
||||||
|
define cabal-exe
|
||||||
|
@echo "[build] $(notdir $@)"
|
||||||
|
cabal build $(1)$(notdir $@) $(CABAL_FLAGS)
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
find dist-newstyle -name $(notdir $@) -type f -exec cp {} $(TMPDIR) \;
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
|
upload: build
|
||||||
|
@echo "[upload]"
|
||||||
|
@rsync --recursive --partial --progress --copy-links \
|
||||||
|
--compress --human-readable --hard-links --size-only \
|
||||||
|
--delete --delete-after \
|
||||||
|
--rsh='ssh -l $(REMOTE_USER) -i $(IDFILE)' \
|
||||||
|
$(BUILDDIR)/ $(HOST):$(REMOTE_DIR)/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean distclean
|
||||||
|
clean:
|
||||||
|
@echo "[clean]"
|
||||||
|
rm -rf $(BUILDDIR)
|
||||||
|
rm -rf $(TMPDIR)
|
||||||
|
distclean: clean
|
||||||
|
@echo "[distclean]"
|
||||||
|
cabal clean
|
||||||
|
|
||||||
|
.SILENT:
|
108
blog-meta/all-tags.hs
Normal file
108
blog-meta/all-tags.hs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Main (main) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as LazyBS
|
||||||
|
import Data.Foldable
|
||||||
|
import Data.Function ((&))
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
import Data.Set (Set)
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Misc
|
||||||
|
import qualified System.FilePath.Find as Find
|
||||||
|
import YAML ((##=), (.!=), (.:))
|
||||||
|
import qualified YAML
|
||||||
|
import System.Environment
|
||||||
|
import qualified Data.Text.IO as Text
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import Data.Char
|
||||||
|
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
Opts {dir, yaml, make} <- getOptions
|
||||||
|
files <- Find.findL True (pure True) (Find.extension Find.==? ".md") dir
|
||||||
|
tags <- traverse getTags files
|
||||||
|
LazyBS.writeFile yaml $ makeYAML tags
|
||||||
|
Text.writeFile make $ makeMake tags
|
||||||
|
|
||||||
|
getTags :: FilePath -> IO (Set Text)
|
||||||
|
getTags file = do
|
||||||
|
yaml <- YAML.readHeader file
|
||||||
|
list <- unwrap file $ YAML.parseEither $
|
||||||
|
yaml & YAML.withMap "yaml header" \m -> m .: "tags" .!= []
|
||||||
|
pure $ Set.fromList list
|
||||||
|
|
||||||
|
makeYAML :: [Set Text] -> LazyBS.ByteString
|
||||||
|
makeYAML tags = "---\n" <> yaml <> "\n...\n" where
|
||||||
|
yaml = YAML.encode1 $ YAML.obj
|
||||||
|
[("title" ##= YAML.str "all tags"),
|
||||||
|
("tags" ##= collate tags)]
|
||||||
|
|
||||||
|
makeMake :: [Set Text] -> Text
|
||||||
|
makeMake tags' = Text.unlines $ build : allPosts : map makeRule tags where
|
||||||
|
build = Text.unwords $
|
||||||
|
"build:" : ["$(BUILDDIR)/" <> t <> ".html" |
|
||||||
|
t <- ["all-tags", "all-posts"] <> map slug' tags]
|
||||||
|
makeRule' opt title file =
|
||||||
|
"$(TMPDIR)/" <> file <> ".md : $(POSTS) $(POST_LISTS)\n\
|
||||||
|
\\t@echo \"[post-lists] $<\"\n\
|
||||||
|
\\t$(POST_LISTS) " <> opt <> " --out $@ \\\n\
|
||||||
|
\\t $(POSTSDIR) \"" <> title <> "\""
|
||||||
|
allPosts = makeRule' "" "all posts" "all-posts"
|
||||||
|
makeRule t =
|
||||||
|
makeRule' ("--tag \"" <> name t <> "\"")
|
||||||
|
("posts tagged ‘" <> name t <> "’")
|
||||||
|
(slug' t)
|
||||||
|
slug' (Tag {slug}) = "tag-" <> slug
|
||||||
|
tags = collate tags'
|
||||||
|
|
||||||
|
data Tag =
|
||||||
|
Tag {
|
||||||
|
name :: !Text,
|
||||||
|
slug :: !Text,
|
||||||
|
count :: !Int
|
||||||
|
}
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance YAML.ToYAML Tag where
|
||||||
|
toYAML (Tag {name, slug, count}) = YAML.obj $
|
||||||
|
[("name" ##= name), ("slug" ##= slug), ("count" ##= count)]
|
||||||
|
|
||||||
|
collate :: [Set Text] -> [Tag]
|
||||||
|
collate tags₀ =
|
||||||
|
toList $ fst $ foldl' add1 (mempty, mempty) $ foldMap toList tags₀
|
||||||
|
where
|
||||||
|
add1 (tags, slugs) name
|
||||||
|
| Map.member name tags =
|
||||||
|
(Map.adjust incrCount name tags, slugs)
|
||||||
|
| otherwise =
|
||||||
|
let tag = makeTag slugs name in
|
||||||
|
(Map.insert name tag tags,
|
||||||
|
Set.insert (slug tag) slugs)
|
||||||
|
makeTag slugs name =
|
||||||
|
Tag {name, slug = makeSlug slugs name, count = 1}
|
||||||
|
makeSlug slugs name = head $ filter (`notElem` slugs) candidates where
|
||||||
|
slug₀ = Text.map toSlugChar name
|
||||||
|
toSlugChar c
|
||||||
|
| isAlphaNum c && isAscii c || c == '-' = toLower c
|
||||||
|
| otherwise = '_'
|
||||||
|
candidates = slug₀ : [slug₀ <> Text.pack (show i) | i <- [(0 :: Int) ..]]
|
||||||
|
incrCount t@(Tag {count}) = t {count = succ count}
|
||||||
|
|
||||||
|
data Options =
|
||||||
|
Opts {
|
||||||
|
dir :: !FilePath,
|
||||||
|
yaml :: !FilePath,
|
||||||
|
make :: !FilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions :: IO Options
|
||||||
|
getOptions = do
|
||||||
|
args <- getArgs
|
||||||
|
prog <- getProgName
|
||||||
|
case args of
|
||||||
|
[dir, yaml, make] -> pure $ Opts {dir, yaml, make}
|
||||||
|
_ -> fail $
|
||||||
|
"usage: " <> prog <> " DIR YAML MAKE ---\n\
|
||||||
|
\ get all tags from posts in DIR and put the results in the given files"
|
51
blog-meta/blog-meta.cabal
Normal file
51
blog-meta/blog-meta.cabal
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
cabal-version: 2.2
|
||||||
|
name: blog-meta
|
||||||
|
version: 0.1
|
||||||
|
|
||||||
|
author: rhiannon morris <rhi@rhiannon.website>
|
||||||
|
maintainer: rhiannon morris <rhi@rhiannon.website>
|
||||||
|
|
||||||
|
common deps
|
||||||
|
default-language: Haskell2010
|
||||||
|
default-extensions:
|
||||||
|
BlockArguments,
|
||||||
|
OverloadedStrings,
|
||||||
|
OverloadedLists,
|
||||||
|
NondecreasingIndentation,
|
||||||
|
ViewPatterns
|
||||||
|
build-depends:
|
||||||
|
base ^>= 4.14.2.0,
|
||||||
|
HsYAML ^>= 0.2.1.0,
|
||||||
|
bytestring ^>= 0.10.12.0,
|
||||||
|
containers ^>= 0.6.4.1,
|
||||||
|
filemanip,
|
||||||
|
pandoc-types ^>= 1.22,
|
||||||
|
text ^>= 1.2.4.1,
|
||||||
|
time ^>= 1.9.3
|
||||||
|
ghc-options: -Wall
|
||||||
|
|
||||||
|
common exe
|
||||||
|
build-depends: blog-meta
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
|
||||||
|
library
|
||||||
|
import: deps
|
||||||
|
hs-source-dirs: lib
|
||||||
|
exposed-modules:
|
||||||
|
YAML,
|
||||||
|
Misc
|
||||||
|
|
||||||
|
executable post-lists
|
||||||
|
import: deps, exe
|
||||||
|
hs-source-dirs: .
|
||||||
|
main-is: post-lists.hs
|
||||||
|
|
||||||
|
executable all-tags
|
||||||
|
import: deps, exe
|
||||||
|
hs-source-dirs: .
|
||||||
|
main-is: all-tags.hs
|
||||||
|
|
||||||
|
executable nice-date
|
||||||
|
import: deps, exe
|
||||||
|
hs-source-dirs: .
|
||||||
|
main-is: nice-date.hs
|
22
blog-meta/lib/Misc.hs
Normal file
22
blog-meta/lib/Misc.hs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module Misc where
|
||||||
|
|
||||||
|
import qualified System.Console.GetOpt as GetOpt
|
||||||
|
import System.Environment
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
-- | exception on 'Left'
|
||||||
|
unwrap :: Show a => FilePath -> Either a b -> IO b
|
||||||
|
unwrap file = either (\x -> fail $ file <> ":" <> show x) return
|
||||||
|
|
||||||
|
getOptionsWith :: (String -> String) -> ([String] -> Maybe a)
|
||||||
|
-> [GetOpt.OptDescr (a -> a)] -> IO a
|
||||||
|
getOptionsWith hdr mkDef descrs = do
|
||||||
|
res <- GetOpt.getOpt GetOpt.Permute descrs <$> getArgs
|
||||||
|
case res of
|
||||||
|
(fs, rest, []) | Just def <- mkDef rest ->
|
||||||
|
return $ foldl (flip ($)) def fs
|
||||||
|
_ -> do
|
||||||
|
prog <- getProgName
|
||||||
|
putStrLn $ GetOpt.usageInfo (hdr prog) descrs
|
||||||
|
exitFailure
|
||||||
|
|
52
blog-meta/lib/YAML.hs
Normal file
52
blog-meta/lib/YAML.hs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
module YAML (module YAML) where
|
||||||
|
|
||||||
|
import Data.YAML as YAML
|
||||||
|
import Data.YAML.Event as YAML (untagged)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import Misc
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LazyBS
|
||||||
|
import qualified System.IO as IO
|
||||||
|
|
||||||
|
|
||||||
|
str' :: String -> Node ()
|
||||||
|
str' = str . Text.pack
|
||||||
|
|
||||||
|
str :: Text -> Node ()
|
||||||
|
str = Scalar () . SStr
|
||||||
|
|
||||||
|
obj :: Mapping () -> Node ()
|
||||||
|
obj = Mapping () untagged
|
||||||
|
|
||||||
|
(##=) :: (ToYAML b) => Text -> b -> (Node (), Node ())
|
||||||
|
(##=) = (#=)
|
||||||
|
|
||||||
|
(#=) :: (ToYAML a, ToYAML b) => a -> b -> (Node (), Node ())
|
||||||
|
k #= v = (toYAML k, toYAML v)
|
||||||
|
|
||||||
|
list :: ToYAML a => [a] -> Node ()
|
||||||
|
list = Sequence () untagged . map toYAML
|
||||||
|
|
||||||
|
|
||||||
|
-- | read a chunk from the beginning of the file between a
|
||||||
|
-- @---@ and a @...@. throw an exception if there isn't one
|
||||||
|
readHeader :: FilePath -> IO (YAML.Node YAML.Pos)
|
||||||
|
readHeader file = IO.withFile file IO.ReadMode \h -> do
|
||||||
|
ln <- BS.hGetLine h
|
||||||
|
if (ln /= "---") then
|
||||||
|
fail $ file <> ": no header"
|
||||||
|
else
|
||||||
|
unwrap file . YAML.decode1 =<< linesUntil "..." h
|
||||||
|
|
||||||
|
-- | read all the lines from a handle until the given terminator. return the
|
||||||
|
-- lines read, excluding the terminator
|
||||||
|
linesUntil :: ByteString -> IO.Handle -> IO LazyBS.ByteString
|
||||||
|
linesUntil end h = go [] where
|
||||||
|
go acc = do
|
||||||
|
l <- BS.hGetLine h
|
||||||
|
if l == end then
|
||||||
|
return $ LazyBS.fromChunks $ reverse acc
|
||||||
|
else
|
||||||
|
go (l <> "\n" : acc)
|
45
blog-meta/nice-date.hs
Normal file
45
blog-meta/nice-date.hs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import Text.Pandoc.Definition
|
||||||
|
import Data.Map.Strict (Map)
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
import Data.Time
|
||||||
|
import Text.Pandoc.JSON
|
||||||
|
import Data.Text (Text, unpack, pack)
|
||||||
|
import Data.Char (toLower)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = toJSONFilter \(Pandoc (Meta m) body) -> do
|
||||||
|
m' <- niceDate m
|
||||||
|
pure $ Pandoc (Meta m') body
|
||||||
|
|
||||||
|
niceDate :: Map Text MetaValue -> IO (Map Text MetaValue)
|
||||||
|
niceDate = Map.alterF reformat "date"
|
||||||
|
|
||||||
|
reformat :: Maybe MetaValue -> IO (Maybe MetaValue)
|
||||||
|
reformat Nothing = pure Nothing
|
||||||
|
reformat (Just (toText -> Just txt)) = do
|
||||||
|
-- extra '-'s in %-m and %-d to allow leading zeroes to be skipped
|
||||||
|
date <- parseTimeM True defaultTimeLocale "%Y-%-m-%-d" $ unpack txt
|
||||||
|
let str = formatTime defaultTimeLocale "%A %-e %B %Y" (date :: Day)
|
||||||
|
pure $ Just $ MetaString $ pack $ map toLower str
|
||||||
|
reformat (Just d) = fail $ "date is\n" <> show d <> "\nwanted a string"
|
||||||
|
|
||||||
|
toText :: MetaValue -> Maybe Text
|
||||||
|
toText (MetaString str) = Just str
|
||||||
|
toText (MetaInlines is) = foldMap inlineText is
|
||||||
|
toText (MetaBlocks bs) = foldMap blockText bs
|
||||||
|
toText _ = Nothing
|
||||||
|
|
||||||
|
inlineText :: Inline -> Maybe Text
|
||||||
|
inlineText (Str txt) = Just txt
|
||||||
|
inlineText Space = Just " "
|
||||||
|
inlineText SoftBreak = Just " "
|
||||||
|
inlineText LineBreak = Just " "
|
||||||
|
inlineText (RawInline _ txt) = Just txt
|
||||||
|
inlineText _ = Nothing
|
||||||
|
|
||||||
|
blockText :: Block -> Maybe Text
|
||||||
|
blockText (Plain is) = foldMap inlineText is
|
||||||
|
blockText (Para is) = foldMap inlineText is
|
||||||
|
blockText Null = Just ""
|
||||||
|
blockText (RawBlock _ txt) = Just txt
|
||||||
|
blockText _ = Nothing
|
100
blog-meta/post-lists.hs
Normal file
100
blog-meta/post-lists.hs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import qualified Data.ByteString.Lazy as LazyBS
|
||||||
|
import Data.Char (toLower)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (sortBy)
|
||||||
|
import Data.Ord (comparing)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import Data.Time
|
||||||
|
import qualified YAML
|
||||||
|
import YAML ((.:), (.!=), (##=))
|
||||||
|
import qualified System.Console.GetOpt as GetOpt
|
||||||
|
import qualified System.FilePath.Find as Find
|
||||||
|
import Misc
|
||||||
|
import Data.Char (toLower)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
Opts title dir tag out <- getOptions
|
||||||
|
files <- Find.findL True (pure True) (Find.extension Find.==? ".md") dir
|
||||||
|
infos <- filter (checkTag tag) <$> traverse getInfo files
|
||||||
|
let content = makeContent title infos
|
||||||
|
case out of
|
||||||
|
Nothing -> LazyBS.putStr content
|
||||||
|
Just fn -> LazyBS.writeFile fn content
|
||||||
|
|
||||||
|
makeContent :: Text -> [PostInfo] -> LazyBS.ByteString
|
||||||
|
makeContent title is' = "---\n" <> YAML.encode1 val <> "...\n" where
|
||||||
|
is = sortBy (flip $ comparing infoDate) is'
|
||||||
|
val = YAML.obj [("title" ##= title), ("posts" ##= is)]
|
||||||
|
|
||||||
|
|
||||||
|
checkTag :: Maybe Text -> PostInfo -> Bool
|
||||||
|
checkTag Nothing _ = True
|
||||||
|
checkTag (Just t) i = t `elem` infoTags i
|
||||||
|
|
||||||
|
|
||||||
|
data Options =
|
||||||
|
Opts {
|
||||||
|
optsTitle :: !Text,
|
||||||
|
optsDir :: !FilePath,
|
||||||
|
optsTag :: !(Maybe Text),
|
||||||
|
optsOut :: !(Maybe FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions :: IO Options
|
||||||
|
getOptions = getOptionsWith hdr defOpts optDescrs where
|
||||||
|
hdr prog = "usage: " <> prog <> " [OPTION...] DIR TITLE\n\
|
||||||
|
\ --- get info about posts in DIR and use given title"
|
||||||
|
|
||||||
|
optDescrs :: [GetOpt.OptDescr (Options -> Options)]
|
||||||
|
optDescrs =
|
||||||
|
[GetOpt.Option "t" ["tag"]
|
||||||
|
(GetOpt.ReqArg (\t o -> o {optsTag = Just $ Text.pack t}) "TAG")
|
||||||
|
"list only posts with the given tag",
|
||||||
|
GetOpt.Option "o" ["out"]
|
||||||
|
(GetOpt.ReqArg (\f o -> o {optsOut = Just f}) "FILE")
|
||||||
|
"write output to FILE"]
|
||||||
|
|
||||||
|
defOpts :: [String] -> Maybe Options
|
||||||
|
defOpts [dir, title] =
|
||||||
|
Just $ Opts {optsDir = dir, optsTitle = Text.pack title,
|
||||||
|
optsTag = Nothing, optsOut = Nothing}
|
||||||
|
defOpts _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
getInfo :: FilePath -> IO PostInfo
|
||||||
|
getInfo file = do
|
||||||
|
yaml <- YAML.readHeader file
|
||||||
|
unwrap file $ YAML.parseEither $
|
||||||
|
yaml & YAML.withMap "title, date, tags" \m ->
|
||||||
|
Info <$> return (Text.pack file)
|
||||||
|
<*> m .: "title"
|
||||||
|
<*> m .: "date"
|
||||||
|
<*> m .: "tags" .!= []
|
||||||
|
|
||||||
|
-- | the front matter info we care about
|
||||||
|
data PostInfo =
|
||||||
|
Info {
|
||||||
|
_nfoFile :: Text,
|
||||||
|
_nfoTitle :: Text,
|
||||||
|
infoDate :: BlogDate,
|
||||||
|
infoTags :: [Text]
|
||||||
|
}
|
||||||
|
|
||||||
|
instance YAML.ToYAML PostInfo where
|
||||||
|
toYAML (Info file title date tags) = YAML.obj
|
||||||
|
[("date" ##= date),
|
||||||
|
("title" ##= title),
|
||||||
|
("tags" ##= tags),
|
||||||
|
("file" ##= file)]
|
||||||
|
|
||||||
|
newtype BlogDate = D Day deriving (Eq, Ord)
|
||||||
|
|
||||||
|
instance YAML.FromYAML BlogDate where
|
||||||
|
parseYAML = YAML.withStr "YYYY-MM-DD" $
|
||||||
|
fmap D . parseTimeM True defaultTimeLocale "%F" . Text.unpack
|
||||||
|
|
||||||
|
instance YAML.ToYAML BlogDate where
|
||||||
|
toYAML (D d) = YAML.str $ Text.pack $ map toLower $
|
||||||
|
formatTime defaultTimeLocale "%a %-d %B %Y" d
|
8
cabal.project
Normal file
8
cabal.project
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
packages:
|
||||||
|
lang/**/*.cabal,
|
||||||
|
./blog-meta/blog-meta.cabal
|
||||||
|
|
||||||
|
source-repository-package
|
||||||
|
type: git
|
||||||
|
location: https://git.rhiannon.website/rhi/filemanip.git
|
||||||
|
tag: 0edef8f7bbfe8e210f546e3222b735a32e6055e3
|
1
lang
Submodule
1
lang
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 3a878590c2a15764d5b6e8d4c80bfbc92714f2ae
|
16
posts/index.md
Normal file
16
posts/index.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
title: test
|
||||||
|
tags: [a, b]
|
||||||
|
date: 2021-07-25
|
||||||
|
conlang: laantas
|
||||||
|
...
|
||||||
|
|
||||||
|
# hello
|
||||||
|
|
||||||
|
im gecs
|
||||||
|
|
||||||
|
:::example
|
||||||
|
`{#kášńḿł | size=100 ; stroke=4}`
|
||||||
|
:::
|
||||||
|
|
||||||
|
$$\mathbb{wow},\, \mathcal{MATH}$$
|
66
style/counters.css
Normal file
66
style/counters.css
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
:root {
|
||||||
|
--section-prefix: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
|
||||||
|
padding-right: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h1 {
|
||||||
|
counter-increment: h1;
|
||||||
|
counter-reset: h2 h3 h4 h5 h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h1::before {
|
||||||
|
content: var(--section-prefix) counter(h1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h2 {
|
||||||
|
counter-increment: h2;
|
||||||
|
counter-reset: h3 h4 h5 h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h2::before {
|
||||||
|
content: var(--section-prefix) counter(h1) '.' counter(h2);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h3 {
|
||||||
|
counter-increment: h3;
|
||||||
|
counter-reset: h4 h5 h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h3::before {
|
||||||
|
content: var(--section-prefix) counter(h1) '.' counter(h2) '.' counter(h3);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h4 {
|
||||||
|
counter-increment: h4;
|
||||||
|
counter-reset: h5 h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h4::before {
|
||||||
|
content: var(--section-prefix)
|
||||||
|
counter(h1) '.' counter(h2) '.' counter(h3) '.' counter(h4);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h5 {
|
||||||
|
counter-increment: h5;
|
||||||
|
counter-reset: h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h5::before {
|
||||||
|
content: var(--section-prefix)
|
||||||
|
counter(h1) '.' counter(h2) '.' counter(h3) '.' counter(h4) '.'
|
||||||
|
counter(h5);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h6 {
|
||||||
|
counter-increment: h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h6::before {
|
||||||
|
content: var(--section-prefix)
|
||||||
|
counter(h1) '.' counter(h2) '.' counter(h3) '.' counter(h4) '.'
|
||||||
|
counter(h5) '.' counter(h6);
|
||||||
|
}
|
||||||
|
|
369
style/page.css
Normal file
369
style/page.css
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
@import url(fonts/muller/muller.css);
|
||||||
|
@import url(fonts/junius/junius.css);
|
||||||
|
@import url(fonts/pragmatapro/pragmatapro.css);
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--root-col: hsl(30deg, 5%, 26%);
|
||||||
|
--fg-col: hsl(336deg, 17%, 11%);
|
||||||
|
--bg-col: hsl(40deg, 91%, 98%);
|
||||||
|
--link-col: hsl(355deg, 52%, 48%);
|
||||||
|
|
||||||
|
--ipa-font: JuniusX;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
background: var(--root-col);
|
||||||
|
|
||||||
|
font-family: Muller;
|
||||||
|
font-size: 16pt;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: url(paper.png), var(--bg-col);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
color: var(--fg-col);
|
||||||
|
box-shadow: 0 0 5em var(--fg-col);
|
||||||
|
|
||||||
|
max-width: 50em;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1em 2em 3em;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 350%;
|
||||||
|
font-weight: 100;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 200%;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 180%;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 160%;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 140%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid var(--root-col);
|
||||||
|
width: 80%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 1em;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 80%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags ul {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags li {
|
||||||
|
list-style: none;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags li + li {
|
||||||
|
margin-left: 0.75ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-col);
|
||||||
|
}
|
||||||
|
|
||||||
|
b, strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
list-style: '— ';
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: auto;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-top: 2px solid var(--root-col);
|
||||||
|
border-bottom: 2px solid var(--root-col);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
border-bottom: 1px solid var(--root-col);
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: PragmataPro;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
width: min-content;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ipa, .lang, .ebnf-t {
|
||||||
|
font-family: var(--ipa-font);
|
||||||
|
|
||||||
|
font-feature-settings:
|
||||||
|
"ccmp", "calt", "liga", "loca", "mark", "mkmk", "ss01";
|
||||||
|
/* ss01 to use modern Þþð design, others to make juniusx go brrr
|
||||||
|
* maybe i can just turn loca off but idk what other regional forms
|
||||||
|
* it does that i actually want */
|
||||||
|
|
||||||
|
text-underline-offset: 0.125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ipa {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang, .ebnf-t {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abbr {
|
||||||
|
font-size: 70%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scr {
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: -40%;
|
||||||
|
padding-right: 0.5ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gloss .scr {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.splash, .example) .scr {
|
||||||
|
height: unset;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example .text {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0.75em auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.letter-list {
|
||||||
|
margin: 1.25em auto 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 80%;
|
||||||
|
row-gap: 0.5em;
|
||||||
|
column-gap: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-list + .letter-list {
|
||||||
|
margin-top: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-list .lang {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-list .scr {
|
||||||
|
height: 3em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-list .text {
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main nav {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#toc > h2 {
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
#toc > ul {
|
||||||
|
columns: 2;
|
||||||
|
margin-left: 2em;
|
||||||
|
margin-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure:not(.left) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
figure:not(.left) table {
|
||||||
|
display: inline-table;
|
||||||
|
}
|
||||||
|
figure table {
|
||||||
|
margin: 1em 0.5em;
|
||||||
|
}
|
||||||
|
.gloss {
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure ul {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 5em;
|
||||||
|
margin-right: 5em;
|
||||||
|
columns: 2;
|
||||||
|
}
|
||||||
|
figure li {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt { font-weight: 500; float: left; clear: left; }
|
||||||
|
dd { margin-left: 4em; }
|
||||||
|
|
||||||
|
dt {
|
||||||
|
break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
break-before: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
u u {
|
||||||
|
text-decoration: double underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.twocol, .threecol):not(:is(ul, ol) *) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.twocol {
|
||||||
|
columns: 2;
|
||||||
|
}
|
||||||
|
.threecol {
|
||||||
|
columns: 3;
|
||||||
|
}
|
||||||
|
.twocol :first-child, .threecol :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin: 1.5em auto 1em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
|
||||||
|
font-size: 80%;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf td {
|
||||||
|
padding: 0 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf-nt {
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(155deg, 80%, 30%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf-punc {
|
||||||
|
color: hsl(25deg, 40%, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf-sub, .ebnf-brack {
|
||||||
|
color: hsl(210deg, 80%, 35%);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf-brack {
|
||||||
|
padding: 0 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ebnf-s {
|
||||||
|
font-style: italic;
|
||||||
|
color: hsl(330deg, 80%, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
max-width: 70%;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
padding-left: 1em;
|
||||||
|
margin: auto;
|
||||||
|
}
|
BIN
style/paper.png
Normal file
BIN
style/paper.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
11
templates/foot.html
Normal file
11
templates/foot.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
$if(hide-footer)$$else$
|
||||||
|
<footer>
|
||||||
|
<hr>
|
||||||
|
<a href=/all-posts.html>all posts</a> ·
|
||||||
|
<a href=/all-tags.html>all tags</a>
|
||||||
|
</footer>
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
$for(include-after)$
|
||||||
|
$include-after$
|
||||||
|
$endfor$
|
24
templates/head.html
Normal file
24
templates/head.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html$if(lang)$ lang=$lang$$endif$$if(dir)$ dir=$dir$$endif$>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
|
||||||
|
<link rel=stylesheet href=/style/page.css>
|
||||||
|
$if(show-toc)$
|
||||||
|
<link rel=stylesheet href=/style/counters.css>
|
||||||
|
$endif$
|
||||||
|
$for(css)$
|
||||||
|
<link rel=stylesheet href=/style/$css$>
|
||||||
|
$endfor$
|
||||||
|
|
||||||
|
$for(header-includes)$
|
||||||
|
$header-includes$
|
||||||
|
$endfor$
|
||||||
|
$if(math)$
|
||||||
|
$math$
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
<title>$pagetitle$</title>
|
||||||
|
|
||||||
|
$for(include-before)$
|
||||||
|
$include-before$
|
||||||
|
$endfor$
|
15
templates/meta.html
Normal file
15
templates/meta.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
$head()$
|
||||||
|
|
||||||
|
$if(title)$
|
||||||
|
<header>
|
||||||
|
<h1>$title$</h1>
|
||||||
|
</header>
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
$if(posts)$
|
||||||
|
$postlist()$
|
||||||
|
$elseif(tags)$
|
||||||
|
$taglist()$
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
$foot()$
|
46
templates/post.html
Normal file
46
templates/post.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
$head()$
|
||||||
|
|
||||||
|
<header>
|
||||||
|
$if(title)$
|
||||||
|
<h1>$title$</h1>
|
||||||
|
$endif$
|
||||||
|
<div class=meta>
|
||||||
|
$if(date)$
|
||||||
|
<h2 class=date>$date$</h2>
|
||||||
|
$endif$
|
||||||
|
$if(tags)$
|
||||||
|
<nav class=tags>
|
||||||
|
tags:
|
||||||
|
<ul>
|
||||||
|
$for(tags)$
|
||||||
|
<li><a href=/tag-$it$.html>$it$</a>
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
$endif$
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
$if(show-toc)$
|
||||||
|
<nav id=toc>
|
||||||
|
<h2>Contents</h2>
|
||||||
|
$table-of-contents$
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
$endif$
|
||||||
|
<main>
|
||||||
|
$body$
|
||||||
|
|
||||||
|
$if(blah)$
|
||||||
|
<dl>
|
||||||
|
$for(blah)$
|
||||||
|
<dt>$blah.a$
|
||||||
|
<dd>$blah.b$
|
||||||
|
$endfor$
|
||||||
|
</dl>
|
||||||
|
$endif$
|
||||||
|
</main>
|
||||||
|
|
||||||
|
$foot()$
|
9
templates/postlist.html
Normal file
9
templates/postlist.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<main>
|
||||||
|
<ul>
|
||||||
|
$for(posts)$
|
||||||
|
<li>
|
||||||
|
<a href=$it.file$.html>$it.title$</a>
|
||||||
|
($it.date$)
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
|
</main>
|
9
templates/taglist.html
Normal file
9
templates/taglist.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<main>
|
||||||
|
<ul>
|
||||||
|
$for(tags)$
|
||||||
|
<li>
|
||||||
|
<a href=tag-$it.slug$.html>$it.name$</a>
|
||||||
|
($it.count$)
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
|
</main>
|
Loading…
Reference in a new issue