Compare commits

...

21 commits
v1 ... main

Author SHA1 Message Date
Pagwin
b4506e7ed5
TODO updates 2025-07-09 16:42:48 -04:00
Pagwin
9ecde51921
lowercase 2025-06-09 16:58:15 -04:00
Pagwin
91ccf173ba
username update 2025-06-09 16:55:52 -04:00
Pagwin
dc17b1e816
ebnf added so parser making can be easier now 2025-06-09 16:29:53 -04:00
Pagwin
4a5d5e541a
new setup for how I'm trying to tackle this 2025-05-09 21:13:22 -04:00
Pagwin
925c70f52b
realizing I should just use cmark and come back if I still don't like something later 2025-05-09 20:32:43 -04:00
Pagwin
07218a32a6
restructuring handling of markdown 2025-05-09 20:08:42 -04:00
Pagwin
2e9860d147
started work on markdown parser 2025-05-08 19:44:43 -04:00
Pagwin
59c6ab4209
change to markdown 2025-04-01 19:27:30 -04:00
Pagwin
e18fee7272
TODO update 2025-04-01 19:27:03 -04:00
Pagwin
1ee03ea495
pageUrl functionality complete 2025-02-23 18:20:28 -05:00
Pagwin
4d45ab4bd5
pageUrl added 2025-02-23 18:05:41 -05:00
Pagwin
52d88c95a7
favicon 2025-02-12 10:20:07 -05:00
Pagwin
2027c8569f
more scaffolding 2025-02-05 12:19:18 -05:00
Pagwin
5bf0487023
scaffolding for yeeting pandoc 2025-02-05 11:21:57 -05:00
Pagwin
82c0fe7631
minor cabal file change 2025-02-05 11:19:45 -05:00
Pagwin
668d2fcad0
sw.js 2025-01-24 18:04:47 -05:00
Pagwin
8fb8b3c176
ripped out typst, next step is to supplant pandoc for markdown with a custom parser 2025-01-16 02:15:49 -05:00
Pagwin
9d776d9c39
is this how github actions are specified? 2025-01-02 01:58:28 -05:00
Pagwin
609cf6c012
added workflow to build container, copied from website repo 2025-01-02 01:31:02 -05:00
Pagwin
301a676d9b
fixed dockerfile 2024-12-30 23:59:56 -05:00
16 changed files with 578 additions and 157 deletions

View file

@ -1,2 +1,3 @@
dist-newstyle
.shake
Dockerfile

56
.github/workflows/build-container.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Build container
on:
push:
branches:
- main
# copied everything below this line from https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: true
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image (versioned)
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ghcr.io/pagwin2/psb:v1
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image (latest)
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ghcr.io/pagwin2/psb:latest
labels: ${{ steps.meta.outputs.labels }}

View file

@ -14,4 +14,6 @@ RUN cabal build
WORKDIR /github/workspace
ENTRYPOINT ["/mnt/dist-newstyle/build/x86_64-linux/ghc-9.4.8/psb-0.1.0.0/x/psb/build/psb/psb", "build", "-p2"]
RUN export folder=$(ls /mnt/dist-newstyle/build/x86_64-linux) && mv /mnt/dist-newstyle/build/x86_64-linux/"$folder"/psb-0.1.0.0/x/psb/build/psb/psb /mnt/psb
ENTRYPOINT ["/mnt/psb", "build"]

23
TODO
View file

@ -1,23 +0,0 @@
github action for deploy to github pages
minify js and css when copying over instead of just copying
make typst dependant on yaml?
add rst support so I'm not dependant on typst not breaking
add a cli flag to send webmentions and track the ones sent/acked in some file, probably a simple csv with path and what got linked to (https://indieweb.org/Webmention) (https://indieweb.org/Webmention-developer)
make it so typst citation section has a header and/or is a div elem instead of a section element
process source code blocks with tree sitter https://hackage.haskell.org/package/tree-sitter
Check to see if pandoc html output can be changed somehow (for header anchor links)
https://hackage.haskell.org/package/pandoc-3.5/docs/Text-Pandoc-Writers.html#t:Writer
HXT on the generated html is an option
https://pandoc.org/MANUAL.html#custom-readers-and-writers might be an option https://pandoc.org/lua-filters.html
most likely will be a generic lua filter though https://pandoc.org/lua-filters.html
see if performance can be improved (it isn't slow atm but it definitely feels like there's a bottleneck)

9
TODO.md Normal file
View file

@ -0,0 +1,9 @@
- add rst support and convert markdown handling to custom parser instead of pandoc
- process source code blocks with tree sitter https://hackage.haskell.org/package/tree-sitter
- minify js and css when copying over instead of just copying
- look into adding postcss support perhaps
- see if performance can be improved (it isn't slow atm but it definitely feels like there's a bottleneck)

View file

@ -3,4 +3,4 @@ name: 'psb'
description: 'Use PSB to build static a static site'
runs:
using: 'docker'
image: 'Dockerfile'
image: 'docker://ghcr.io/pagwin2/psb:v1'

View file

@ -4,11 +4,11 @@ outputDir :: String
outputDir = "publish"
assetGlobs :: [String]
assetGlobs = ["static//*", "robots.txt"]
assetGlobs = ["static//*", "robots.txt", "sw.js", "favicon.ico"]
-- CAN ONLY BE TYPST DOCS UNLESS YOU CHANGE THINGS AT THE `pages` RULE in `Main.hs
pagePaths :: [String]
pagePaths = []
postGlobs :: [String]
postGlobs = ["posts/*.typ", "posts/*.md"]
postGlobs = ["posts/*.md"]

18
app/IR.hs Normal file
View file

@ -0,0 +1,18 @@
module IR where
import Data.Text
-- Html and Math tags come with their data because they are leaves for us
-- We aren't parsing that if we can avoid it
data Tag = Heading {level :: Int} | Paragraph | Blockquote | Code | Html {html :: Text} | Anchor | Italic | Bold | Math {mathML :: Text}
data Data = Ast {ast :: AST} | Text {text :: Text}
data AST = AST {tag :: Tag, child :: [Data]}
-- for processing math
-- https://hackage.haskell.org/package/typst-0.6.1/docs/Typst-Parse.html#v:parseTypst
-- and
-- https://hackage.haskell.org/package/typst-symbols-0.1.7/docs/Typst-Symbols.html
-- are going to be used for handling typst and
-- texmath for latex handling

View file

@ -11,7 +11,6 @@ import Config
import Control.Monad (forM, when)
import qualified Data.HashMap.Strict as HM
import Data.List (sortOn)
import Data.Maybe (fromJust)
import qualified Data.Ord as Ord
import qualified Data.Text as T
import Deriving.Aeson
@ -20,7 +19,6 @@ import Development.Shake (Action, Rules, (%>), (|%>), (~>))
import qualified Development.Shake as Shake
import Development.Shake.FilePath ((</>))
import qualified Development.Shake.FilePath as FP
import qualified Development.Shake.FilePath as Shake
import Templates
import Types
import Utilities
@ -59,7 +57,6 @@ buildRules :: Rules ()
buildRules = do
home
assets
pages
postsRule
rss
@ -70,28 +67,6 @@ assets =
let src = FP.dropDirectory1 target
Shake.copyFileChanged src target
-- Shake.putInfo $ "Copied " <> target <> " from " <> src
-- handling typst only because pages should only be typst no reason for backwards compat on that
pages :: Rules ()
pages =
map indexHtmlOutputPath pagePaths |%> \target -> do
let src = indexHtmlTypstSourcePath target
let metaSrc = indexHtmlTypstMetaPath target
html <- typstToHtml src
meta <- yamlToPost metaSrc
time <- Utilities.now
let page =
Page
{ pageTitle = postTitle meta,
pageContent = html,
pageNow = time,
pageSection = T.pack $ fromJust $ Shake.stripExtension "html" target
}
applyTemplateAndWrite "default.html" page target
-- Shake.putInfo $ "Built " <> target <> " from " <> src
-- there's probably a better way of doing this that allows for the target's origin file extension to get passed in but for now we're doing brute force
postsRule :: Rules ()
postsRule =
@ -105,33 +80,12 @@ postsRule =
when
should
( case FP.takeExtension path of
".typ" -> typstPost path
".md" -> markdownPost path
_ -> error $ "invalid file extension for post " <> target
)
)
return ()
typstPost :: FP.FilePath -> Action ()
typstPost src = do
Shake.need [src]
let target = indexHtmlOutputPath src
post <- readTypstPost src
let rPost = fromPost post
postHtml <- applyTemplate "post.html" rPost
time <- Utilities.now
let page =
Page
{ pageTitle = rPostTitle rPost,
pageContent = postHtml,
pageNow = time,
pageSection = T.pack $ fromJust $ Shake.stripExtension "html" target
}
applyTemplateAndWrite "default.html" page target
-- Shake.putInfo $ "Built " <> target <> " from " <> src
markdownPost :: FP.FilePath -> Action ()
markdownPost src = do
Shake.need [src]
@ -139,16 +93,16 @@ markdownPost src = do
post <- readMarkdownPost src
let rPost = fromPost post
-- Shake.putInfo $ show . toJSON $ rPost
postHtml <- applyTemplate "post.html" rPost
time <- Utilities.now
-- Shake.putInfo $ T.unpack $ urlConvert target
let page =
Page
{ pageTitle = rPostTitle rPost,
pageContent = postHtml,
pageNow = time,
pageSection = T.pack $ fromJust $ Shake.stripExtension "html" target
pageUrl = urlConvert target
}
applyTemplateAndWrite "default.html" page target
@ -164,12 +118,13 @@ home =
let posts' = map fromPost posts
html <- applyTemplate "home.html" $ HM.singleton "posts" posts'
time <- Utilities.now
-- Shake.putInfo $ T.unpack $ urlConvert target
let page =
Page
{ pageTitle = T.pack "Home",
pageContent = html,
pageNow = time,
pageSection = T.pack $ fromJust $ Shake.stripExtension "html" target
pageUrl = urlConvert target
}
applyTemplateAndWrite "default.html" page target
@ -195,21 +150,9 @@ rss =
readPost :: FilePath -> Action Post
readPost postPath = do
case FP.takeExtension postPath of
".typ" -> readTypstPost postPath
".md" -> readMarkdownPost postPath
_ -> error $ "unknown file extension for file" <> postPath
readTypstPost :: FilePath -> Action Post
readTypstPost postPath = do
html <- typstToHtml postPath
post <- yamlToPost $ typstMetaPath postPath
-- Shake.putInfo $ "Read " <> postPath
return $
post
{ postContent = Just html,
postLink = Just . T.pack $ "/" <> FP.dropExtension postPath <> "/"
}
readMarkdownPost :: FilePath -> Action Post
readMarkdownPost postPath = do
(post, html) <- markdownToHtml postPath

5
app/Markdown.hs Normal file
View file

@ -0,0 +1,5 @@
{-# LANGUAGE OverloadedStrings #-}
module Markdown () where
import CMark

35
app/Restruct.hs Normal file
View file

@ -0,0 +1,35 @@
module Restruct where
-- https://docutils.sourceforge.io/rst.html
-- https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
-- https://hackage.haskell.org/package/parsec-3.1.18.0/docs/doc-index-All.html
import Data.Text (Text)
import Data.Void (Void)
import Text.Parsec as P
data RestElement
= RBody RestBody
| RTransition
| -- list of integers is the location in the section heirachy it is, Text is the title
-- NOTE: future me don't bother with proper restext convention do header depth via #n prefix to the title
RSection [Int] Text RestBody
data RestBody
= RParagraph [RInlineText]
| RBulletList Void
| REnumList Void
| RDefinitionList Void
| RFieldList Void
| ROptionList Void
| RLiteralBlock Void
| RLineBlock Void
| RBlockQuote Void
| -- skipping doctest blocks because no I'll just use a literal block thanks
RTable Void
| RExplicit Void
data MarkupModifier = Underline | Bold | Italic
data RInlineText = RInLineText {text :: Text, modifiers :: [MarkupModifier]}

View file

@ -10,8 +10,8 @@ data Page = Page
pageContent :: Text,
-- build time
pageNow :: Text,
-- css class for page section
pageSection :: Text
--
pageUrl :: Text
}
deriving (Show, Generic)
deriving (ToJSON) via PrefixedSnake "page" Page

View file

@ -25,14 +25,7 @@ indexHtmlOutputPath srcPath =
-- were applicative shenanigans necessary? no
-- but using them felt cool
indexHtmlSourcePaths :: FilePath -> [FilePath]
indexHtmlSourcePaths path = [indexHtmlTypstSourcePath, indexHtmlMarkdownSourcePath] <*> [path]
indexHtmlTypstSourcePath :: FilePath -> FilePath
indexHtmlTypstSourcePath =
FP.dropDirectory1
. (<.> "typ")
. FP.dropTrailingPathSeparator
. FP.dropFileName
indexHtmlSourcePaths path = [indexHtmlMarkdownSourcePath] <*> [path]
indexHtmlMarkdownSourcePath :: FilePath -> FilePath
indexHtmlMarkdownSourcePath =
@ -41,25 +34,6 @@ indexHtmlMarkdownSourcePath =
. FP.dropTrailingPathSeparator
. FP.dropFileName
indexHtmlTypstMetaPath :: FilePath -> FilePath
indexHtmlTypstMetaPath = typstMetaPath . indexHtmlTypstSourcePath
typstMetaPath :: FilePath -> FilePath
typstMetaPath typstPath = FP.dropExtension typstPath <.> "yaml"
typstToHtml :: FilePath -> Action Text
typstToHtml filePath = do
content <- Shake.readFile' filePath
Shake.quietly . Shake.traced "Typst to HTML" $ do
doc <- runPandoc . Pandoc.readTypst readerOptions . T.pack $ content
html <- runPandoc . Pandoc.writeHtml5String writerOptions $ doc
return html
where
readerOptions =
Pandoc.def {Pandoc.readerExtensions = Pandoc.pandocExtensions}
writerOptions =
Pandoc.def {Pandoc.writerExtensions = Pandoc.pandocExtensions}
markdownToHtml :: (FromJSON a) => FilePath -> Action (a, Text)
markdownToHtml filePath = do
content <- Shake.readFile' filePath
@ -137,14 +111,11 @@ yamlToPost path = do
-- let post' = dateTransform post
return post
isTypstPost :: FilePath -> Bool
isTypstPost path = FP.takeExtension path == ".typ"
isMarkdownPost :: FilePath -> Bool
isMarkdownPost path = FP.takeExtension path == ".md"
postHandles :: [(FilePath -> Bool, FilePath -> Action Post)]
postHandles = [(isTypstPost, yamlToPost . typstMetaPath), (isMarkdownPost, markdownToPost)]
postHandles = [(isMarkdownPost, markdownToPost)]
isDraft :: FilePath -> Action Bool
isDraft path = do
@ -167,3 +138,6 @@ parseDate str = do
date <- parseTimeM False defaultTimeLocale "%Y-%-m-%-d" $ T.unpack str
-- need to append the time to avoid potential issues
return $ T.pack $ formatTime @UTCTime defaultTimeLocale "%Y-%m-%dT00:00:00Z" date
urlConvert :: FilePath -> Text
urlConvert = T.pack . FP.dropFileName . flip FP.replaceDirectory1 "https://pagwin.xyz"

148
markdown.ebnf Normal file
View file

@ -0,0 +1,148 @@
(* Markdown EBNF Grammar *)
document = { block } ;
block = heading
| horizontal_rule
| code_block
| quote_block
| list
| table
| paragraph
| blank_line ;
(* Headings *)
heading = atx_heading | setext_heading ;
atx_heading = "#" { "#" } [ " " ] inline_text newline ;
setext_heading = inline_text newline
( ( "=" { "=" } ) | ( "-" { "-" } ) ) newline ;
(* Horizontal Rule *)
horizontal_rule = ( ( "*" [ " " ] "*" [ " " ] "*" { [ " " ] "*" } )
| ( "-" [ " " ] "-" [ " " ] "-" { [ " " ] "-" } )
| ( "_" [ " " ] "_" [ " " ] "_" { [ " " ] "_" } ) ) newline ;
(* Code Blocks *)
code_block = fenced_code_block | indented_code_block ;
fenced_code_block = "```" [ language_identifier ] newline
{ code_line }
"```" newline ;
indented_code_block = { " " code_line } ;
code_line = { character - newline } newline ;
language_identifier = { letter | digit | "-" | "+" } ;
(* Quote Blocks *)
quote_block = { ">" [ " " ] ( inline_text | "" ) newline } ;
(* Lists *)
list = unordered_list | ordered_list ;
unordered_list = { unordered_list_item } ;
ordered_list = { ordered_list_item } ;
unordered_list_item = [ " " { " " } ] ( "*" | "+" | "-" ) " " inline_text newline
{ continuation_line } ;
ordered_list_item = [ " " { " " } ] digit { digit } "." " " inline_text newline
{ continuation_line } ;
continuation_line = " " inline_text newline ;
(* Tables *)
table = table_header table_separator { table_row } ;
table_header = "|" { table_cell "|" } newline ;
table_separator = "|" { table_align_spec "|" } newline ;
table_row = "|" { table_cell "|" } newline ;
table_cell = { character - ( "|" | newline ) } ;
table_align_spec = [ ":" ] "-" { "-" } [ ":" ] ;
(* Paragraphs *)
paragraph = inline_text { newline inline_text } newline ;
(* Inline Elements *)
inline_text = { inline_element } ;
inline_element = emphasis
| strong
| code_span
| link
| image
| autolink
| line_break
| plain_text ;
emphasis = ( "*" non_asterisk_text "*" )
| ( "_" non_underscore_text "_" ) ;
strong = ( "**" non_asterisk_text "**" )
| ( "__" non_underscore_text "__" ) ;
code_span = "`" { "`" } non_backtick_text { "`" } "`" ;
link = "[" link_text "]" "(" link_url [ " " link_title ] ")" ;
image = "!" "[" alt_text "]" "(" image_url [ " " image_title ] ")" ;
autolink = "<" ( url | email ) ">" ;
line_break = " " newline | "\\" newline ;
(* Text Content *)
plain_text = { character - special_char } ;
non_asterisk_text = { character - "*" } ;
non_underscore_text = { character - "_" } ;
non_backtick_text = { character - "`" } ;
link_text = { character - ( "[" | "]" ) } ;
alt_text = { character - ( "[" | "]" ) } ;
link_url = { character - ( "(" | ")" | " " ) } ;
image_url = { character - ( "(" | ")" | " " ) } ;
link_title = quote_string ;
image_title = quote_string ;
quote_string = ( '"' { character - '"' } '"' )
| ( "'" { character - "'" } "'" ) ;
url = "http" [ "s" ] "://" { character - ">" } ;
email = { character - ( "@" | ">" ) } "@" { character - ">" } ;
(* Utilities *)
blank_line = newline ;
special_char = "*" | "_" | "`" | "[" | "]" | "(" | ")" | "#" | ">" | "|" | "!" | "\\" ;
newline = "\n" | "\r\n" ;
character = letter | digit | symbol | " " ;
letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
| "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
| "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
| "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
symbol = "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "_" | "="
| "+" | "[" | "]" | "{" | "}" | "\\" | "|" | ";" | ":" | "'" | '"' | "," | "."
| "<" | ">" | "/" | "?" | "~" | "`" ;

View file

@ -1,16 +1,4 @@
cabal-version: 3.4
-- The cabal-version field refers to the version of the .cabal specification,
-- and can be different from the cabal-install (the tool) version and the
-- Cabal (the library) version you are using. As such, the Cabal (the library)
-- version used must be equal or greater than the version stated in this field.
-- Starting from the specification version 2.2, the cabal-version field must be
-- the first thing in the cabal file.
-- Initial package description 'psb' generated by
-- 'cabal init'. For further documentation, see:
-- http://haskell.org/cabal/users-guide/
--
-- The name of the package.
name: psb
-- The package version.
@ -22,35 +10,15 @@ name: psb
-- | | | +--- code changes with no API change
version: 0.1.0.0
-- A short (one-line) description of the package.
-- synopsis:
-- A longer description of the package.
-- description:
-- The license under which the package is released.
license: MIT
-- The file containing the license text.
license-file: LICENSE
-- The package author(s).
author: Pagwin
-- An email address to which users can send suggestions, bug reports, and patches.
maintainer: dev@pagwin.xyz
-- A copyright notice.
-- copyright:
category: Web
build-type: Simple
-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
extra-doc-files: CHANGELOG.md
-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
-- extra-source-files:
common warnings
ghc-options: -Wall
@ -61,14 +29,14 @@ executable psb
-- .hs or .lhs file containing the Main module.
main-is: Main.hs
-- Modules included in this executable, other than Main.
other-modules: Config Utilities Templates Types
other-modules: Config Utilities Templates Types IR Markdown Restruct
-- LANGUAGE extensions used by modules in this package.
default-extensions: ApplicativeDo DataKinds NamedFieldPuns DerivingVia LambdaCase TypeApplications DeriveGeneric
-- Other library packages from which modules are imported.
build-depends: base >=4.17.2.1, mustache >=2.4.2, pandoc >=3.2.1, shake >= 0.19.8, deriving-aeson >= 0.2.9, aeson, text, time, unordered-containers, yaml
-- https://hackage.haskell.org/package/texmath
-- cmark is pinned because I don't want to touch it unless I rewrite to my own code
build-depends: base >=4.17.2.1, mustache >=2.4.2, pandoc >=3.2.1, shake >= 0.19.8, deriving-aeson >= 0.2.9, aeson, text, time, unordered-containers, yaml, parsec >= 3.1.18.0, typst >= 0.6.1, typst-symbols >= 0.1.7, cmark == 0.6.1
-- Directories containing source files.
hs-source-dirs: app

285
restructured.ebnf Normal file
View file

@ -0,0 +1,285 @@
(* reStructuredText EBNF Grammar *)
document = { block } ;
block = section
| transition
| paragraph
| literal_block
| line_block
| block_quote
| doctest_block
| table
| bullet_list
| enumerated_list
| definition_list
| field_list
| option_list
| directive
| comment
| substitution_definition
| target
| blank_line ;
(* Sections *)
section = section_title section_underline [ section_overline ] ;
section_title = inline_text newline ;
section_underline = section_adornment newline ;
section_overline = section_adornment newline ;
section_adornment = adornment_char { adornment_char } ;
adornment_char = "!" | '"' | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\\" | "]" | "^" | "_" | "`" | "{" | "|" | "}" | "~" ;
(* Transitions *)
transition = transition_marker newline ;
transition_marker = transition_char { transition_char } ;
transition_char = adornment_char ;
(* Paragraphs *)
paragraph = inline_text { newline inline_text } newline ;
(* Literal Blocks *)
literal_block = literal_block_marker newline { indented_line } ;
literal_block_marker = "::" | paragraph "::" ;
indented_line = indent line_content newline ;
indent = " " | "\t" ;
line_content = { character - newline } ;
(* Line Blocks *)
line_block = { line_block_line } ;
line_block_line = "|" [ " " ] inline_text newline ;
(* Block Quotes *)
block_quote = { indented_paragraph } [ attribution ] ;
indented_paragraph = indent inline_text { newline indent inline_text } newline ;
attribution = indent "-- " inline_text newline ;
(* Doctest Blocks *)
doctest_block = { doctest_line } ;
doctest_line = ">>>" " " line_content newline
| "..." " " line_content newline ;
(* Tables *)
table = simple_table | grid_table ;
simple_table = simple_table_row { simple_table_row } simple_table_separator { simple_table_row } ;
simple_table_row = { table_cell } newline ;
simple_table_separator = "=" { ( "=" | " " ) } newline ;
grid_table = grid_table_border { grid_table_row grid_table_border } ;
grid_table_border = "+" { ( "-" | "+" ) } newline ;
grid_table_row = "|" { table_cell "|" } newline ;
table_cell = { character - ( "|" | newline ) } ;
(* Lists *)
bullet_list = { bullet_list_item } ;
bullet_list_item = bullet_marker " " list_item_content ;
bullet_marker = "*" | "+" | "-" | "•" | "‣" | "" ;
enumerated_list = { enumerated_list_item } ;
enumerated_list_item = enumeration_marker " " list_item_content ;
enumeration_marker = ( digit { digit } "." )
| ( digit { digit } ")" )
| ( "(" digit { digit } ")" )
| ( letter "." )
| ( letter ")" )
| ( "(" letter ")" )
| ( roman "." )
| ( roman ")" )
| ( "(" roman ")" )
| "#." | "#)" | "(#)" ;
list_item_content = inline_text { newline [ indent ] inline_text } newline ;
definition_list = { definition_list_item } ;
definition_list_item = term newline indent definition newline ;
term = inline_text ;
definition = inline_text { newline indent inline_text } ;
(* Field Lists *)
field_list = { field_list_item } ;
field_list_item = ":" field_name ":" " " field_body newline ;
field_name = { letter | digit | " " | "-" | "_" } ;
field_body = inline_text { newline indent inline_text } ;
(* Option Lists *)
option_list = { option_list_item } ;
option_list_item = option_group " " option_description newline ;
option_group = option { ", " option } ;
option = short_option | long_option ;
short_option = "-" letter [ " " option_argument ] ;
long_option = "--" { letter | digit | "-" } [ "=" option_argument ] ;
option_argument = { letter | digit | "-" | "_" } ;
option_description = inline_text { newline indent inline_text } ;
(* Directives *)
directive = ".." " " directive_name "::" [ " " directive_arguments ] newline
[ directive_options ]
[ blank_line ]
[ directive_content ] ;
directive_name = { letter | digit | "-" | "_" } ;
directive_arguments = { character - newline } ;
directive_options = { directive_option } ;
directive_option = indent ":" option_name ":" [ " " option_value ] newline ;
option_name = { letter | digit | "-" | "_" } ;
option_value = { character - newline } ;
directive_content = { indented_line } ;
(* Comments *)
comment = ".." [ " " comment_text ] newline { indented_line } ;
comment_text = { character - newline } ;
(* Substitution Definitions *)
substitution_definition = ".." " " "|" substitution_name "|" " " directive_name "::" [ " " directive_arguments ] newline
[ directive_options ]
[ directive_content ] ;
substitution_name = { character - ( "|" | newline ) } ;
(* Targets *)
target = internal_target | external_target ;
internal_target = ".." " " "_" target_name ":" newline ;
external_target = ".." " " "_" target_name ":" " " target_url newline ;
target_name = { character - ( ":" | newline ) } ;
target_url = { character - newline } ;
(* Inline Elements *)
inline_text = { inline_element } ;
inline_element = emphasis
| strong
| literal
| interpreted_text
| phrase_reference
| substitution_reference
| inline_internal_target
| hyperlink_reference
| footnote_reference
| citation_reference
| inline_literal
| plain_text ;
emphasis = "*" emphasis_text "*" ;
strong = "**" strong_text "**" ;
literal = "``" literal_text "``" ;
interpreted_text = "`" interpreted_text_content "`" [ role_suffix ]
| role_prefix "`" interpreted_text_content "`" ;
role_prefix = ":" role_name ":" ;
role_suffix = ":" role_name ":" ;
role_name = { letter | digit | "-" | "_" | "." } ;
interpreted_text_content = { character - "`" } ;
phrase_reference = "`" phrase_reference_text "`_" [ "_" ] ;
phrase_reference_text = { character - ( "`" | "<" ) } [ " " "<" target_url ">" ] ;
substitution_reference = "|" substitution_name "|" [ "_" [ "_" ] ] ;
inline_internal_target = "_`" target_text "`" ;
target_text = { character - "`" } ;
hyperlink_reference = reference_name "_" [ "_" ] ;
reference_name = { letter | digit | "-" | "_" | "." } ;
footnote_reference = "[" footnote_label "]_" ;
footnote_label = digit { digit } | "#" [ footnote_name ] | "*" ;
footnote_name = { letter | digit | "-" | "_" } ;
citation_reference = "[" citation_label "]_" ;
citation_label = { letter | digit | "-" | "_" | "." } ;
inline_literal = "`" "`" literal_content "`" "`" ;
literal_content = { character - "`" } ;
(* Text Content *)
plain_text = { character - markup_char } ;
emphasis_text = { character - ( "*" | newline ) } ;
strong_text = { character - ( "*" | newline ) } ;
literal_text = { character - ( "`" | newline ) } ;
markup_char = "*" | "`" | "_" | "|" | "[" | "]" | ":" | "." | ">" | "<" ;
(* Utilities *)
blank_line = newline ;
roman = "i" | "ii" | "iii" | "iv" | "v" | "vi" | "vii" | "viii" | "ix" | "x"
| "I" | "II" | "III" | "IV" | "V" | "VI" | "VII" | "VIII" | "IX" | "X" ;
newline = "\n" | "\r\n" ;
character = letter | digit | symbol | " " ;
letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
| "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
| "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
| "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
symbol = "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "_" | "="
| "+" | "[" | "]" | "{" | "}" | "\\" | "|" | ";" | ":" | "'" | '"' | "," | "."
| "<" | ">" | "/" | "?" | "~" | "`" ;