├── src ├── build.mli ├── dune ├── url.mli ├── otoml_impl.ml ├── filesystem.ml ├── url.ml ├── serve.ml ├── config.ml ├── highlight.ml └── build.ml ├── example ├── content │ ├── site.css │ ├── posts │ │ ├── index.md │ │ ├── first-post.md │ │ ├── second-post.md │ │ └── third-post.lagda.md │ ├── index.md │ └── agda.lagda.md ├── example.agda-lib ├── includes │ ├── nav.jingoo │ └── head.jingoo ├── layouts │ ├── main.jingoo │ ├── tag.jingoo │ ├── posts.jingoo │ └── post.jingoo └── config.toml ├── .gitignore ├── test └── unit │ ├── dune │ ├── test_color.ml │ └── test_url.ml ├── web ├── content │ ├── favicon.ico │ ├── docs │ │ ├── commands.md │ │ ├── highlighting.md │ │ ├── taxonomies.md │ │ ├── index.md │ │ ├── configuration.md │ │ ├── templates.md │ │ └── pages.md │ ├── index.css │ ├── docs.css │ ├── index.html │ ├── site.css │ └── logo.svg ├── config.toml ├── includes │ ├── footer.jingoo │ ├── head.jingoo │ └── header.jingoo ├── layouts │ ├── home.jingoo │ └── docs.jingoo ├── README.md ├── markdown.tmLanguage-LICENSE ├── theme.tmTheme-LICENSE ├── grammars │ └── TOML.tmLanguage └── theme.tmTheme ├── bin ├── dune └── main.ml ├── .ocamlformat ├── README.md ├── .github └── workflows │ ├── pages.yaml │ └── test.yaml ├── dune-project ├── LICENSE ├── camyll.opam └── CHANGES.md /src/build.mli: -------------------------------------------------------------------------------- 1 | val build : unit -> unit 2 | -------------------------------------------------------------------------------- /example/content/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | .merlin 3 | web/public 4 | example/public 5 | *.agdai 6 | -------------------------------------------------------------------------------- /test/unit/dune: -------------------------------------------------------------------------------- 1 | (tests 2 | (names test_url test_color) 3 | (libraries camyll)) 4 | -------------------------------------------------------------------------------- /example/example.agda-lib: -------------------------------------------------------------------------------- 1 | name: example 2 | depend: standard-library 3 | include: content 4 | -------------------------------------------------------------------------------- /example/content/posts/index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Posts" 3 | layout = "posts.jingoo" 4 | +++ 5 | -------------------------------------------------------------------------------- /web/content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alan-j-hu/camyll/HEAD/web/content/favicon.ico -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name camyll) 4 | (libraries camyll cmdliner unix)) 5 | -------------------------------------------------------------------------------- /example/includes/nav.jingoo: -------------------------------------------------------------------------------- 1 |
2 | Home 3 | Posts 4 |
5 | -------------------------------------------------------------------------------- /example/content/index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Home" 3 | layout = "main.jingoo" 4 | +++ 5 | 6 | Welcome to my example blog! 7 | -------------------------------------------------------------------------------- /web/config.toml: -------------------------------------------------------------------------------- 1 | source_dir = "site" 2 | dest_dir = "public" 3 | agda_dir = "lagda" 4 | exclude = ["*.agdai"] 5 | taxonomies = [] 6 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name camyll) 3 | (libraries calendar cmarkit ezjsonm httpaf httpaf-lwt-unix ISO8601 jingoo 4 | lwt markup otoml plist-xml re slug textmate-language uri yaml)) 5 | -------------------------------------------------------------------------------- /example/includes/head.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ frontmatter.title }} 5 | -------------------------------------------------------------------------------- /example/content/posts/first-post.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "First Post" 3 | layout = "post.jingoo" 4 | date = 2020-01-01 5 | 6 | [taxonomies] 7 | categories = ["Posts"] 8 | +++ 9 | 10 | Hello world! 11 | -------------------------------------------------------------------------------- /src/url.mli: -------------------------------------------------------------------------------- 1 | val relativize : src:string -> dest:string -> string 2 | (** If [dest] begins with /, returns a URL equivalent to [dest] relative to URL 3 | [src]. If [dest] does not begin with /, returns [dest]. *) 4 | -------------------------------------------------------------------------------- /example/layouts/main.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | {% include "nav.jingoo" %} 8 | {{ content }} 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/content/posts/second-post.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Second Post" 3 | layout = "post.jingoo" 4 | date = 2020-02-29 5 | 6 | [taxonomies] 7 | categories = ["Posts"] 8 | tags = ["Foo", "bar"] 9 | +++ 10 | 11 | This is the second post! 12 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | profile=conventional 2 | ocaml-version=5.0.0 3 | break-cases=toplevel 4 | break-separators=after 5 | cases-exp-indent=2 6 | margin=79 7 | dock-collection-brackets 8 | space-around-records 9 | space-around-lists 10 | space-around-arrays 11 | -------------------------------------------------------------------------------- /example/config.toml: -------------------------------------------------------------------------------- 1 | source_dir = "site" 2 | dest_dir = "public" 3 | agda_dir = "lagda" 4 | exclude = ["*.agdai"] 5 | 6 | [[taxonomies]] 7 | name = "categories" 8 | layout = "tag.jingoo" 9 | 10 | [[taxonomies]] 11 | name = "tags" 12 | layout = "tag.jingoo" 13 | -------------------------------------------------------------------------------- /web/includes/footer.jingoo: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /example/content/agda.lagda.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Agda" 3 | layout = "main.jingoo" 4 | +++ 5 | 6 | Hello Agda! 7 | 8 | ``` 9 | module agda where 10 | 11 | open import posts.third-post using (Nat) 12 | ``` 13 | 14 | **Bold** 15 | 16 | ```agda 17 | N : Set 18 | N = Nat 19 | ``` 20 | -------------------------------------------------------------------------------- /web/includes/head.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ frontmatter.title }} 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camyll 2 | 3 | Camyll is a static site generator. 4 | 5 | Features: 6 | 7 | - Conversion from Markdown to HTML 8 | - Syntax highlighting of any language via user-provided TextMate grammars 9 | - Post tagging 10 | - Processing of Literate Agda 11 | 12 | ## Installing 13 | 14 | opam install camyll 15 | -------------------------------------------------------------------------------- /web/layouts/home.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | 8 | {% include "header.jingoo" %} 9 | {{ content }} 10 | {% include "footer.jingoo" %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/content/docs/commands.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Commands" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Commands 7 | 8 | Camyll has the following subcommands: 9 | 10 | - `camyll build`: Builds the site. 11 | - `camyll serve [--port=PORT (default 8080)]`: Serves the site at the given 12 | port. 13 | 14 | Each subcommand accepts the flag `--help` to print documentation. 15 | -------------------------------------------------------------------------------- /example/content/posts/third-post.lagda.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Third Post" 3 | layout = "post.jingoo" 4 | date = 2021-01-01 5 | 6 | [taxonomies] 7 | categories = ["Posts"] 8 | tags = ["foo", "bar baz"] 9 | +++ 10 | 11 | Hello Agda! 12 | 13 | ```agda 14 | module posts.third-post where 15 | 16 | open import Data.Nat using (ℕ; zero; suc) 17 | ``` 18 | 19 | **Bold** 20 | 21 | ```agda 22 | Nat : Set 23 | Nat = ℕ 24 | ``` 25 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | TOML syntax highlighting file taken from 2 | https://github.com/textmate/toml.tmbundle. 3 | 4 | Markdown syntax highlighting file taken from 5 | https://github.com/Microsoft/vscode-markdown-tm-grammar. Its license is located 6 | at `markdown.tmLanguage-LICENSE`. 7 | 8 | The TextMate theme is GitHub Dark theme, taken from 9 | https://github.com/primer/github-textmate-theme. Its license is located at 10 | `theme.tmTheme-LICENSE`. 11 | -------------------------------------------------------------------------------- /web/includes/header.jingoo: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | -------------------------------------------------------------------------------- /test/unit/test_color.ml: -------------------------------------------------------------------------------- 1 | open Camyll.Highlight 2 | 3 | let () = 4 | assert (validate_color "#000000" = Ok ()); 5 | assert (validate_color "#999999" = Ok ()); 6 | assert (validate_color "#123456" = Ok ()); 7 | assert (validate_color "#123" = Ok ()); 8 | assert (validate_color "#5555" = Ok ()); 9 | assert (validate_color "#FfFf12A4" = Ok ()) 10 | 11 | let () = 12 | assert (Result.is_error (validate_color "#")); 13 | assert (Result.is_error (validate_color "#12345")); 14 | assert (Result.is_error (validate_color "#gggggg")) 15 | -------------------------------------------------------------------------------- /example/layouts/tag.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | {% include "nav.jingoo" %} 8 |

{{ name }}

9 | {{ content }} 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/layouts/posts.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | {% include "nav.jingoo" %} 8 |

{{ frontmatter.title }}

9 | {{ content }} 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/content/index.css: -------------------------------------------------------------------------------- 1 | div.big-logo { 2 | background-color: #c5dbe8; 3 | text-align: center; 4 | margin: 0; 5 | } 6 | 7 | .big-logo img { 8 | text-align: center; 9 | margin-top: 2em; 10 | margin-bottom: 0; 11 | padding: 0; 12 | height: 10em; 13 | } 14 | 15 | #container { 16 | background-color: #f0f0f0; 17 | } 18 | 19 | #features { 20 | max-width: 1000px; 21 | margin: 0 auto; 22 | padding: 1em; 23 | display: flex; 24 | } 25 | 26 | .feature { 27 | flex: 1; 28 | padding: 1em; 29 | } 30 | 31 | @media (max-width: 700px) { 32 | #features { 33 | display: block; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/layouts/post.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | {% include "nav.jingoo" %} 8 | Published: {{ format_date("%B %_d, %Y", frontmatter.date) }}
9 | Tags: 10 | [ 11 | {%- set ns = namespace (sep = false) -%} 12 | {%- for tag in frontmatter.taxonomies.tags -%} 13 | {%- if ns.sep -%} 14 | , 15 | {% else -%} 16 | {%- set ns.sep = true -%} 17 | {%- endif -%} 18 | {{ tag }} 19 | {%- endfor -%} 20 | ] 21 | {{ content }} 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/content/docs.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100%; 3 | background-color: #f0f0f0; 4 | border: 0; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | .main > div { 10 | display: flex; 11 | width: 80%; 12 | margin: 0 auto; 13 | } 14 | 15 | nav.sidebar { 16 | width: 20%; 17 | margin: 10px; 18 | } 19 | 20 | @media (max-width: 700px) { 21 | .main > div { 22 | flex-direction: column; 23 | } 24 | 25 | main { 26 | width: 100%; 27 | } 28 | 29 | nav.sidebar { 30 | width: 100%; 31 | } 32 | } 33 | 34 | .sidebar ul { 35 | list-style: none; 36 | } 37 | 38 | main { 39 | padding: 2em 5%; 40 | width: 80%; 41 | margin: 10px; 42 | } 43 | -------------------------------------------------------------------------------- /src/otoml_impl.ml: -------------------------------------------------------------------------------- 1 | module ISODate = struct 2 | type t = float 3 | 4 | let parse date = fst (ISO8601.Permissive.datetime_tz ~reqtime:false date) 5 | let local_time_of_string = parse 6 | let local_date_of_string = parse 7 | let local_datetime_of_string = parse 8 | let offset_datetime_of_string = parse 9 | let local_time_to_string = ISO8601.Permissive.string_of_time 10 | let local_date_to_string = ISO8601.Permissive.string_of_date 11 | let local_datetime_to_string = ISO8601.Permissive.string_of_datetime 12 | let offset_datetime_to_string = ISO8601.Permissive.string_of_datetime 13 | end 14 | 15 | module T = Otoml.Base.Make (Otoml.Base.OCamlNumber) (ISODate) 16 | -------------------------------------------------------------------------------- /web/content/docs/highlighting.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Syntax Highlighting" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Syntax Highlighting 7 | 8 | Camyll supports syntax highlighting using TextMate themes and grammars. If a 9 | TextMate theme file called `theme.tmTheme` exists in the project directory, 10 | Camyll will use it to highlight Markdown fenced code blocks. Put the grammars 11 | for the languages you want to highlight in the directory specified in the 12 | [configuration](configuration.html), and Camyll will find them. When searching 13 | for a language, Camyll will first search case-insensitively by the `name` 14 | attribute of the grammar files, then search case-sensitively by the `fileTypes` 15 | attribute of the grammar files. 16 | -------------------------------------------------------------------------------- /.github/workflows/pages.yaml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up OCaml 15 | uses: ocaml/setup-ocaml@v3 16 | with: 17 | ocaml-compiler: 5.3.x 18 | 19 | - name: Build 20 | run: | 21 | sudo apt-get install libonig-dev 22 | eval $(opam env) 23 | opam install ./camyll.opam 24 | cd web 25 | camyll build 26 | 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v4 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./web/public 32 | -------------------------------------------------------------------------------- /src/filesystem.ml: -------------------------------------------------------------------------------- 1 | (* If the directory already exists, does nothing. *) 2 | let touch_dir name = if not (Sys.file_exists name) then Sys.mkdir name 0o777 3 | let split_re = Re.compile (Re.str Filename.dir_sep) 4 | 5 | (* Creates all directories in the current path. *) 6 | let create_dirs path = 7 | let split = Re.split split_re path in 8 | ignore 9 | (List.fold_left 10 | (fun path name -> 11 | let path = Filename.concat path name in 12 | touch_dir path; 13 | path) 14 | "" split) 15 | 16 | let rec remove_dir dirname = 17 | Sys.readdir dirname 18 | |> Array.iter (fun name -> remove (Filename.concat dirname name)); 19 | Sys.rmdir dirname 20 | 21 | and remove name = 22 | if Sys.is_directory name then remove_dir name else Sys.remove name 23 | -------------------------------------------------------------------------------- /web/content/docs/taxonomies.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Taxonomies" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Taxonomies 7 | 8 | Camyll supports arbitrary, user-defined page classifications. A *taxonomy* is 9 | a classification, such as "categories" or "tags." A *taxonomy term* is a name 10 | for a set of pages that is associated with a particular taxonomy, such as 11 | a tag called "OCaml" or a category called "programming." 12 | 13 | Taxonomies must be declared in the [configuration file](configuration.html). 14 | Camyll will generate a page for each taxonomy term at the address 15 | `//` using the template specified in the configuration 16 | file. 17 | 18 | A [page's frontmatter](pages.html) lists the terms that the page belongs to. 19 | Each term belongs to the namespace of one taxonomy. 20 | -------------------------------------------------------------------------------- /web/layouts/docs.jingoo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.jingoo" %} 5 | 6 | 7 | 8 | {% include "header.jingoo" %} 9 |
10 |
11 | 22 |
23 | {{ content }} 24 |
25 |
26 |
27 | {% include "footer.jingoo" %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /bin/main.ml: -------------------------------------------------------------------------------- 1 | open Camyll 2 | open Cmdliner 3 | 4 | let guard f x = 5 | try Ok (f x) with 6 | | Failure e -> Error e 7 | | Invalid_argument e -> Error e 8 | | Sys_error e -> Error e 9 | | Unix.Unix_error (e, cmd, "") -> Error (cmd ^ ": " ^ Unix.error_message e) 10 | | Unix.Unix_error (e, cmd, p) -> 11 | Error (cmd ^ " " ^ p ^ ": " ^ Unix.error_message e) 12 | 13 | let build_cmd = 14 | let doc = "build the site" in 15 | Cmd.v (Cmd.info "build" ~doc) 16 | Term.(term_result' (const (guard Build.build) $ const ())) 17 | 18 | let serve_cmd = 19 | let doc = "serve the site" in 20 | let port = 21 | let inf = Arg.info ~docv:"PORT" [ "port" ] in 22 | Arg.(value & opt int 8080 inf) 23 | in 24 | Cmd.v (Cmd.info "serve" ~doc) 25 | Term.(term_result' (const (guard Serve.serve) $ port)) 26 | 27 | let () = 28 | let doc = "static site generator" in 29 | let cmds = [ build_cmd; serve_cmd ] in 30 | exit Cmd.(eval (group (Cmd.info "camyll" ~doc) cmds)) 31 | -------------------------------------------------------------------------------- /web/content/docs/index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Documentation" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Documentation 7 | 8 | ## Installation 9 | 10 | The easiest way to install Camyll is through Opam. 11 | 12 | ``` 13 | opam install camyll 14 | ``` 15 | 16 | ## Directory Structure 17 | 18 | A site directory should look like this: 19 | 20 | ``` 21 | . 22 | ├── config.toml 23 | ├── content/ 24 | ├── grammars/ 25 | ├── includes/ 26 | ├── layouts/ 27 | └── theme.tmTheme 28 | ``` 29 | 30 | - `config.toml` is the configuration file. 31 | - `content/` is a directory that contains the site content to be transformed. 32 | - `grammars/` is a directory that contains the TextMate grammars used for 33 | syntax highlighting. 34 | - `includes/` is a directory that contains partial templates that may be 35 | included in other templates. 36 | - `layouts/` is a directory that contains whole-page templates. 37 | - `theme.tmTheme` is a file that contains the TextMate theme used for syntax 38 | highlighting. 39 | -------------------------------------------------------------------------------- /test/unit/test_url.ml: -------------------------------------------------------------------------------- 1 | open Camyll.Url 2 | 3 | let () = 4 | assert (relativize ~src:"/posts/first-post/" ~dest:"/posts/" = "../"); 5 | assert (relativize ~src:"/posts/" ~dest:"/posts/first-post/" = "first-post/"); 6 | assert ( 7 | relativize ~src:"/posts/first-post/" ~dest:"/posts/second-post/" 8 | = "../second-post/"); 9 | assert ( 10 | relativize ~src:"/posts/first-post/" ~dest:"posts/second-post/" 11 | = "posts/second-post/"); 12 | assert (relativize ~src:"/posts/first-post/" ~dest:"" = ""); 13 | assert (relativize ~src:"/" ~dest:"/docs/index.html" = "docs/index.html"); 14 | assert ( 15 | relativize ~src:"/docs/index.html" ~dest:"/docs/configuration.html" 16 | = "configuration.html"); 17 | assert (relativize ~src:"/docs/index.html" ~dest:"/site.css" = "../site.css"); 18 | assert (relativize ~src:"/posts/second-post.html" ~dest:"/posts/" = "./"); 19 | assert ( 20 | relativize ~src:"/posts/third-post.html" ~dest:"/posts/third-post.html" 21 | = "third-post.html") 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: install-test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-24.04 8 | strategy: 9 | matrix: 10 | ocaml-compiler: 11 | - 4.14.x 12 | - 5.3.x 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: ocaml/setup-ocaml@v3 16 | with: 17 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 18 | - name: Update path 19 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH 20 | 21 | - name: Build 22 | run: | 23 | eval $(opam env) 24 | sudo apt-get install agda-bin 25 | sudo apt-get install libonig-dev 26 | pip3 install agda-pkg 27 | echo "$HOME/.local/bin" >> $GITHUB_PATH 28 | apkg init 29 | apkg install standard-library --version v1.1 --yes 30 | opam install . --with-test 31 | opam install ocamlformat 32 | dune fmt 33 | cd example 34 | camyll build 35 | cd ../web 36 | camyll build 37 | -------------------------------------------------------------------------------- /web/content/index.html: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Camyll" 3 | layout = "home.jingoo" 4 | +++ 5 | 6 | 9 | 10 |
11 |
12 |
13 |

Markdown

14 | Camyll converts files ending in .md from Markdown to HTML. 15 |
16 |
17 |

Literate Agda

18 | Camyll recognizes files ending in .lagda.md as Literate Agda 19 | files and invokes the Agda compiler to preprocess the Agda code blocks. 20 |
21 |
22 |

Syntax Highlighting

23 | Instead of supporting a fixed set of languages, Camyll lets the user 24 | provide TextMate grammars in the site directory for any desired language. 25 | Camyll uses TextMate themes to assign colors to tokens. 26 |
27 |
28 |

Tags and Taxonomies

29 | Camyll supports tags and user-defined taxonomies. 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.7) 2 | (name camyll) 3 | (generate_opam_files true) 4 | 5 | (license MIT) 6 | (authors "Alan Hu ") 7 | (maintainers "Alan Hu ") 8 | (homepage "https://alan-j-hu.github.io/camyll") 9 | (source (github alan-j-hu/camyll)) 10 | 11 | (package 12 | (name camyll) 13 | (synopsis "A static site generator") 14 | (description "Camyll is a static site generator. 15 | 16 | Features: 17 | 18 | - Conversion from Markdown to HTML 19 | - Syntax highlighting of any language via user-provided TextMate grammars 20 | - Post tagging 21 | - Processing of literate Agda") 22 | (tags (blog web website)) 23 | (depends 24 | (angstrom (>= 0.15)) 25 | (calendar (>= 2.01)) 26 | (cmarkit (>= 0.3.0)) 27 | (cmdliner (>= 1.1)) 28 | (ezjsonm (>= 1.3)) 29 | (httpaf (>= 0.7.1)) 30 | (httpaf-lwt-unix (>= 0.7.1)) 31 | (jingoo (>= 1.4)) 32 | (markup (>= 0.8)) 33 | (ocaml (>= 4.14)) 34 | (otoml (>= 0.9.3)) 35 | (plist-xml (>= 0.5)) 36 | (re (>= 1.9)) 37 | (slug (>= 1.0)) 38 | (textmate-language (>= 0.3.2)) 39 | (uri (>= 4.2)) 40 | (yaml (>= 3.1)))) 41 | -------------------------------------------------------------------------------- /web/content/docs/configuration.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Configuration" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Configuration 7 | 8 | Camyll uses [TOML](https://toml.io/en/) for configuration. A configuration file 9 | has the following format: 10 | 11 | ```toml 12 | dest_dir = "" # : string 13 | 14 | agda_dir = "" # : string 15 | 16 | exclude = [] # : string list 17 | 18 | taxonomies = {} # : { name : string; layout : string } list 19 | ``` 20 | 21 | ## dest_dir 22 | 23 | `dest_dir` is the name of the directory that contains the rendered site. 24 | 25 | ## agda_dir 26 | 27 | `agda_dir` is the name of the directory (inside the destination directory) that 28 | contains the generated Literate Agda documentation for libraries. 29 | 30 | ## exclude 31 | 32 | `exclude` is a list of globs of files to ignore. 33 | 34 | ## taxonomies 35 | 36 | `taxonomies` is the list of [taxonomies](taxonomies.html). Each taxonomy 37 | contains the key `name`, which is its name, used in the URL, and the key 38 | `layout`, which is the name of the layout template to use to generate the page 39 | of an individual taxonomy term. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2025 Alan Hu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/url.ml: -------------------------------------------------------------------------------- 1 | let relativize ~src ~dest = 2 | let chop_common_prefix url1 url2 = 3 | let rec loop url1 url2 = 4 | match (url1, url2) with 5 | | x :: xs, y :: ys when x = y -> loop xs ys 6 | | url1, url2 -> (url1, url2) 7 | in 8 | loop url1 url2 9 | in 10 | if String.length dest > 0 && String.get dest 0 = '/' then 11 | let src_dir = 12 | (* chop the filename off the end of the source path *) 13 | match List.rev (String.split_on_char '/' src) with 14 | | _ :: xs -> List.rev xs 15 | | [] -> failwith "Unreachable" 16 | in 17 | let dest_file, dest_dir = 18 | (* chop the filename off the end of the dest path *) 19 | match List.rev (String.split_on_char '/' dest) with 20 | | x :: xs -> (x, List.rev xs) 21 | | [] -> failwith "Unreachable" 22 | in 23 | let src_dir, dest_dir = chop_common_prefix src_dir dest_dir in 24 | let url = 25 | [ dest_file ] |> ( @ ) dest_dir 26 | |> List.rev_append (List.init (List.length src_dir) (Fun.const "..")) 27 | |> String.concat "/" 28 | in 29 | if url = "" then "./" else url 30 | else dest 31 | -------------------------------------------------------------------------------- /web/markdown.tmLanguage-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/theme.tmTheme-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 GitHub, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/content/docs/templates.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Templates" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Templates 7 | 8 | Camyll uses the [Jingoo]( 9 | http://tategakibunko.github.io/jingoo/templates/templates.en.html) templating 10 | engine. Camyll has two types of templates: 11 | 12 | - Layouts, which are whole-page templates. 13 | - Includes, which are partial templates that are included in other templates. 14 | 15 | ## Built-ins 16 | 17 | In addition to the functions provided by Jingoo, Camyll offers two additional 18 | built-in functions: 19 | 20 | `format_date(format : string, date : float) : string` 21 | 22 | Formats a date given in Unix time. The format string is [that of the 23 | Calendar library]( 24 | https://github.com/ocaml-community/calendar/blob/a447a88ae3c1e9873e32d2a95d3d3e7c5ed4a7da/src/printer.mli#L34), 25 | which itself closely follows the Unix `date` command. 26 | 27 | `slugify(str : string) : string` 28 | 29 | "Slugifies" a string for use in a URL, consistent with Camyll's internal slugify 30 | operation. This function is useful for getting the URL of a tag page. 31 | 32 | ## Pages 33 | 34 | Individual pages have the following names in scope: 35 | 36 | - `content` - The content of the page, expressed in HTML. 37 | - `frontmatter` - The page frontmatter. 38 | - `pages` - All the pages in the current directory. 39 | Each page is an object of the following format: 40 | - `frontmatter` - The frontmatter of the post 41 | - `url` - The URL of the post 42 | -------------------------------------------------------------------------------- /camyll.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A static site generator" 4 | description: """ 5 | Camyll is a static site generator. 6 | 7 | Features: 8 | 9 | - Conversion from Markdown to HTML 10 | - Syntax highlighting of any language via user-provided TextMate grammars 11 | - Post tagging 12 | - Processing of literate Agda""" 13 | maintainer: ["Alan Hu "] 14 | authors: ["Alan Hu "] 15 | license: "MIT" 16 | tags: ["blog" "web" "website"] 17 | homepage: "https://alan-j-hu.github.io/camyll" 18 | bug-reports: "https://github.com/alan-j-hu/camyll/issues" 19 | depends: [ 20 | "dune" {>= "2.7"} 21 | "angstrom" {>= "0.15"} 22 | "calendar" {>= "2.01"} 23 | "cmarkit" {>= "0.3.0"} 24 | "cmdliner" {>= "1.1"} 25 | "ezjsonm" {>= "1.3"} 26 | "httpaf" {>= "0.7.1"} 27 | "httpaf-lwt-unix" {>= "0.7.1"} 28 | "jingoo" {>= "1.4"} 29 | "markup" {>= "0.8"} 30 | "ocaml" {>= "4.14"} 31 | "otoml" {>= "0.9.3"} 32 | "plist-xml" {>= "0.5"} 33 | "re" {>= "1.9"} 34 | "slug" {>= "1.0"} 35 | "textmate-language" {>= "0.3.2"} 36 | "uri" {>= "4.2"} 37 | "yaml" {>= "3.1"} 38 | "odoc" {with-doc} 39 | ] 40 | build: [ 41 | ["dune" "subst"] {dev} 42 | [ 43 | "dune" 44 | "build" 45 | "-p" 46 | name 47 | "-j" 48 | jobs 49 | "@install" 50 | "@runtest" {with-test} 51 | "@doc" {with-doc} 52 | ] 53 | ] 54 | dev-repo: "git+https://github.com/alan-j-hu/camyll.git" 55 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.4.4 (April 6, 2025) 2 | 3 | - Switch Markdown engine from OMD to Cmarkit. 4 | 5 | ## 0.4.3 (March 14, 2023) 6 | 7 | - Upgrade `plist-xml` to version 0.5. 8 | 9 | ## 0.4.2 (February 4, 2023) 10 | 11 | - Remove dependency on `lambdasoup`, allowing Camyll to build on OCaml 5 12 | - Use `In_channel` and `Out_channel` modules, requiring OCaml 4.14 or higher 13 | 14 | ## 0.4.1 (October 29, 2022) 15 | 16 | - Upgrade `calendar`, `cmdliner`, and `textmate-language` dependencies and 17 | replace usage of deprecated `cmdliner` API. 18 | - Catch stray exceptions. 19 | - Support JSON and YAML grammar files. 20 | 21 | ## 0.4.0 (November 28, 2021) 22 | 23 | - Switch from To.ml to OTOML library (#1, Daniil Baturin). 24 | - Close a leaked file handle. 25 | - Use `slug` library instead of handrolled function for generating slugs. 26 | 27 | ## 0.3.0 (July 28, 2020) 28 | 29 | - Validate colors from TextMate themes. 30 | - Implement other theme attributes. 31 | - Use proper URI parsing library in server; previous code didn't handle ? and 32 | #. 33 | - Use opinionated directory names instead of making them configurable. 34 | - Determine Agda module names by parsing file instead of deriving it from the 35 | filepath. This makes it possible to set the project root in a different 36 | directory (such as through `.agda-lib`). 37 | - Don't leave Agda-processed Markdown files in the Agda documentation directory. 38 | 39 | ## 0.2.0 (July 21, 2020) 40 | 41 | - Switch template language from Mustache to Jingoo. 42 | - Switch config language from YAML to TOML. 43 | - Require frontmatter. 44 | - Add the `serve` command for serving a site. 45 | - Add taxonomies. 46 | - Use TextMate themes for syntax highlighting. 47 | 48 | ## 0.1.0 (October 15, 2020) 49 | 50 | Initial release. 51 | -------------------------------------------------------------------------------- /src/serve.ml: -------------------------------------------------------------------------------- 1 | module C = Config 2 | 3 | (* Opening Httpaf shadows Config *) 4 | open Httpaf 5 | open Httpaf_lwt_unix 6 | 7 | let serve_file config path = 8 | let path = Filename.concat config.C.dest_dir path in 9 | try 10 | if Sys.is_directory path then 11 | let path = Filename.concat path "index.html" in 12 | Some (In_channel.with_open_bin path In_channel.input_all) 13 | else Some (In_channel.with_open_bin path In_channel.input_all) 14 | with Sys_error _ -> None 15 | 16 | let request_handler config _ reqd = 17 | let { Request.meth; target; _ } = Reqd.request reqd in 18 | match meth with 19 | | `GET -> ( 20 | let path = target |> Uri.of_string |> Uri.path in 21 | match serve_file config path with 22 | | Some response_body -> 23 | let headers = 24 | Headers.of_list 25 | [ ("Content-length", string_of_int (String.length response_body)) ] 26 | in 27 | Reqd.respond_with_string reqd 28 | (Response.create ~headers `OK) 29 | response_body 30 | | None -> 31 | let headers = Headers.of_list [ ("Connection", "close") ] in 32 | Reqd.respond_with_string reqd (Response.create ~headers `Not_found) "") 33 | | _ -> 34 | let headers = Headers.of_list [ ("Connection", "close") ] in 35 | Reqd.respond_with_string reqd 36 | (Response.create ~headers `Method_not_allowed) 37 | "" 38 | 39 | let error_handler _ ?request:_ _ _f = () 40 | 41 | let serve_with_config config port = 42 | let listen_address = Unix.ADDR_INET (Unix.inet_addr_loopback, port) in 43 | let _server = 44 | Lwt_io.establish_server_with_client_socket listen_address 45 | (Server.create_connection_handler 46 | ~request_handler:(request_handler config) ~error_handler) 47 | in 48 | prerr_endline 49 | ("Now listening on port " ^ Int.to_string port 50 | ^ "... Press CTRL+C to stop."); 51 | let forever, _ = Lwt.wait () in 52 | Lwt_main.run forever 53 | 54 | let serve port = C.with_config serve_with_config port 55 | -------------------------------------------------------------------------------- /src/config.ml: -------------------------------------------------------------------------------- 1 | module Otoml = Otoml_impl.T 2 | 3 | type taxonomy = { name : string; layout : string } 4 | 5 | type t = { 6 | dest_dir : string; 7 | exclude : Re.re list; 8 | agda_dir : string; 9 | taxonomies : taxonomy list; 10 | } 11 | 12 | let agda_dest t = Filename.concat t.dest_dir t.agda_dir 13 | 14 | let ( let+ ) opt f = 15 | match opt with 16 | | Some x -> Some (f x) 17 | | None -> None 18 | 19 | let ( and+ ) lhs rhs = 20 | match (lhs, rhs) with 21 | | Some lhs, Some rhs -> Some (lhs, rhs) 22 | | _, _ -> None 23 | 24 | let ( and* ) = ( and+ ) 25 | 26 | let ( let* ) opt f = 27 | match opt with 28 | | Some x -> f x 29 | | None -> None 30 | 31 | let rec mapM f = function 32 | | [] -> Some [] 33 | | x :: xs -> ( 34 | match f x with 35 | | None -> None 36 | | Some y -> ( 37 | match mapM f xs with 38 | | None -> None 39 | | Some xs -> Some (y :: xs))) 40 | 41 | let taxonomy_of_toml toml = 42 | let+ name = Otoml.find_opt toml Otoml.get_string [ "name" ] 43 | and+ layout = Otoml.find_opt toml Otoml.get_string [ "layout" ] in 44 | { name; layout } 45 | 46 | let of_toml toml = 47 | let* dest_dir = Otoml.find_opt toml Otoml.get_string [ "dest_dir" ] 48 | and* agda_dir = Otoml.find_opt toml Otoml.get_string [ "agda_dir" ] 49 | and* exclude = 50 | Otoml.find_opt toml (Otoml.get_array Otoml.get_string) [ "exclude" ] 51 | and* taxonomies = 52 | Otoml.find_opt toml (Otoml.get_array Otoml.get_value) [ "taxonomies" ] 53 | in 54 | let+ taxonomies = mapM taxonomy_of_toml taxonomies in 55 | { 56 | dest_dir; 57 | agda_dir; 58 | exclude = List.map (fun g -> Re.compile (Re.Glob.glob g)) exclude; 59 | taxonomies; 60 | } 61 | 62 | let with_config f = 63 | let config = 64 | match Otoml.Parser.from_file_result "config.toml" with 65 | | Error e -> failwith e 66 | | Ok toml -> ( 67 | match of_toml toml with 68 | | Some config -> config 69 | | None -> failwith "Could not read config.toml") 70 | in 71 | f config 72 | -------------------------------------------------------------------------------- /web/content/docs/pages.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Pages" 3 | layout = "docs.jingoo" 4 | +++ 5 | 6 | # Pages 7 | 8 | *Pages* are files that have a one-to-one mapping with the webpages of the 9 | generated site. At the top of a page, there must be a pair of `+++`s. In 10 | between the `+++` is the page's *frontmatter*, which is written in 11 | [TOML](https://toml.io/en/). For example: 12 | 13 | ```markdown 14 | +++ 15 | title = "Pages" 16 | layout = "main.jingoo" 17 | +++ 18 | 19 | # Pages 20 | 21 | *Pages* are files that have a one-to-one mapping with the webpages of the 22 | generated site. At the top of a page, there must be a pair of `+++`s. In 23 | between the `+++` is the page's *frontmatter*, which is written in 24 | [TOML](https://toml.io/en/). For example: 25 | ``` 26 | 27 | ## Frontmatter 28 | 29 | The frontmatter attributes that Camyll uses are: 30 | 31 | ```toml 32 | # The Jingoo template to use 33 | layout = # : string 34 | # A map from taxonomies to a list of taxonomy terms. 35 | taxonomies = # : { string -> string list } 36 | ``` 37 | 38 | Other attributes can be added to the frontmatter, and they will be accessible 39 | from the Jingoo template. An enhanced frontmatter could look like this: 40 | 41 | ```toml 42 | title = "Introduction to OCaml" 43 | layout = "post.jingoo" 44 | date = 2021-01-05 45 | 46 | [taxonomies] 47 | categories = ["Programming"] 48 | tags = ["OCaml", "Functional programming"] 49 | ``` 50 | 51 | This frontmatter describes a page that will be transformed according to the 52 | template "post.jingoo", belongs to category "Programming", and has tags "OCaml" 53 | and "Functional programming". In addition, the page has a title of "Introduction 54 | to OCaml" and an associated date of January 5th, 2021. 55 | 56 | ## File Extensions 57 | 58 | The file extension of a page determines how Camyll handles it. The supported 59 | file extensions are: 60 | 61 | - `.html`: HTML pages are left as-is. 62 | - `.md`: Markdown files are translated to HTML. 63 | - `.lagda.md`: Literate Agda Markdown files are transformed into regular 64 | Markdown files by the Agda compiler, then handled like other Markdown files. 65 | -------------------------------------------------------------------------------- /web/content/site.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | background-color: #333; 7 | border: 0; 8 | margin: 0; 9 | padding: 0; 10 | font-family: sans-serif; 11 | font-size: 1rem; 12 | line-height: 1.5; 13 | /* Without these two attributes, mobile browsers give some lines of formatted 14 | code larger fonts. */ 15 | -webkit-text-size-adjust: 100%; 16 | text-size-adjust: none; 17 | } 18 | 19 | a[href] { 20 | color: #e57600; 21 | text-decoration: none; 22 | } 23 | 24 | a[href]:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | header { 29 | background-color: #222; 30 | width: 100%; 31 | box-shadow: 0 0 5px #222; 32 | line-height: 3rem; 33 | white-space: nowrap; 34 | text-align: center; 35 | } 36 | 37 | .logo { 38 | color: white; 39 | padding: 0 1rem; 40 | display: inline-block; 41 | vertical-align: middle; 42 | } 43 | 44 | .logo img { 45 | max-height: 2.5rem; 46 | vertical-align: middle; 47 | } 48 | 49 | nav.nav { 50 | display: inline-block; 51 | border: 0; 52 | margin: 0 auto; 53 | } 54 | 55 | .nav ul { 56 | display: inline; 57 | margin: 0; 58 | padding: 0; 59 | } 60 | 61 | .nav li { 62 | display: inline-block; 63 | height: 100%; 64 | } 65 | 66 | .nav a { 67 | color: white; 68 | display: inline-block; 69 | height: 100%; 70 | padding: 0 1rem; 71 | text-align: center; 72 | text-decoration: none; 73 | } 74 | 75 | .nav a:hover { 76 | color: white; 77 | background-color: #f48a32; 78 | } 79 | 80 | @media screen and (max-width: 700px) { 81 | .logo { 82 | display: block; 83 | } 84 | } 85 | 86 | h1, h2, h3, h4, h5, h6 { 87 | margin: 0; 88 | text-align: center; 89 | } 90 | 91 | h1 { 92 | font-size: 1.5rem; 93 | } 94 | 95 | h2 { 96 | font-size: 1.3rem; 97 | } 98 | 99 | code { 100 | font-size: 1rem; 101 | } 102 | 103 | pre code { 104 | border: none; 105 | border-radius: 0; 106 | } 107 | 108 | p code, li code { 109 | padding: 0 5px; 110 | border: 1px solid #c0c0c0; 111 | border-radius: 5px; 112 | } 113 | 114 | p { 115 | margin-bottom: 1.5rem; 116 | } 117 | 118 | pre { 119 | line-height: 1; 120 | overflow: auto; 121 | padding: 5px; 122 | } 123 | 124 | footer { 125 | color: white; 126 | text-align: center; 127 | padding: 1em; 128 | } 129 | -------------------------------------------------------------------------------- /web/content/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 40 | 42 | 46 | 48 | 53 | 65 | 68 | 70 | 78 | 90 | 91 | 94 | 102 | 114 | 115 | 116 | 119 | 121 | 129 | 141 | 142 | 145 | 153 | 165 | 166 | 167 | 172 | 175 | 182 | 189 | 201 | 202 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/highlight.ml: -------------------------------------------------------------------------------- 1 | type scope = string list 2 | type scope_stack = scope list 3 | type selector = { select : scope_stack } 4 | 5 | type token = { 6 | background : string option; 7 | foreground : string option; 8 | is_bold : bool; 9 | is_italics : bool; 10 | is_underline : bool; 11 | selectors : selector list; 12 | } 13 | 14 | type theme = { 15 | background : string option; 16 | foreground : string option; 17 | tokens : token list; 18 | } 19 | 20 | let find_exn key obj = 21 | match List.assoc_opt key obj with 22 | | Some v -> v 23 | | None -> failwith (key ^ " not found.") 24 | 25 | let get_dict = function 26 | | `Dict d -> d 27 | | _ -> failwith "Type error: Expected dict." 28 | 29 | let get_list f = function 30 | | `Array l -> List.map f l 31 | | _ -> failwith "Type error: Expected list." 32 | 33 | let get_string = function 34 | | `String s -> s 35 | | _ -> failwith "Type error: Expected string." 36 | 37 | (* Checks if a string represents a valid CSS color. *) 38 | let validate_color str = 39 | let open Angstrom in 40 | let is_hexadecimal = function 41 | | '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true 42 | | _ -> false 43 | in 44 | let rgb_maybe_a = 45 | char '#' 46 | *> (count 3 (satisfy is_hexadecimal) *> end_of_input 47 | <|> count 4 (satisfy is_hexadecimal) *> end_of_input 48 | <|> count 6 (satisfy is_hexadecimal) *> end_of_input 49 | <|> count 8 (satisfy is_hexadecimal) *> end_of_input) 50 | in 51 | parse_string ~consume:All rgb_maybe_a str 52 | 53 | let get_styles str = 54 | let tokens = str |> String.split_on_char ' ' |> List.filter (( <> ) "") in 55 | let rec loop ~is_bold ~is_italic ~is_underline = function 56 | | [] -> (is_bold, is_italic, is_underline) 57 | | "bold" :: tokens -> loop ~is_bold:true ~is_italic ~is_underline tokens 58 | | "italic" :: tokens -> loop ~is_bold ~is_italic:true ~is_underline tokens 59 | | "underline" :: tokens -> 60 | loop ~is_bold ~is_italic ~is_underline:true tokens 61 | | token :: _ -> raise (Invalid_argument ("Unknown style " ^ token ^ "!")) 62 | in 63 | loop ~is_bold:false ~is_italic:false ~is_underline:false tokens 64 | 65 | let validate_color_exn c = 66 | let str = get_string c in 67 | match validate_color str with 68 | | Ok () -> str 69 | | Error e -> raise (Invalid_argument ("Invalid color: " ^ str ^ " " ^ e)) 70 | 71 | let token_of_plist (plist : Plist_xml.t) : token option = 72 | (* TODO: Handle selector substraction operator *) 73 | let make select = { select } in 74 | let d = get_dict plist in 75 | match List.assoc_opt "scope" d with 76 | | None -> None 77 | | Some scope -> 78 | let selectors = 79 | scope |> get_string |> String.split_on_char ',' 80 | |> List.map (fun str -> 81 | str |> String.split_on_char ' ' |> List.map String.trim 82 | |> List.filter (( <> ) "") 83 | |> List.rev 84 | |> List.map (String.split_on_char '.') 85 | |> make) 86 | in 87 | let settings = find_exn "settings" d |> get_dict in 88 | let is_bold, is_italics, is_underline = 89 | match List.assoc_opt "fontStyle" settings with 90 | | None -> (false, false, false) 91 | | Some styles -> get_styles (get_string styles) 92 | in 93 | Some 94 | { 95 | background = 96 | Option.map validate_color_exn (List.assoc_opt "background" settings); 97 | foreground = 98 | Option.map validate_color_exn (List.assoc_opt "foreground" settings); 99 | is_bold; 100 | is_italics; 101 | is_underline; 102 | selectors; 103 | } 104 | 105 | let theme_of_plist plist = 106 | let d = get_dict plist in 107 | let tokens = find_exn "settings" d in 108 | let tokens = get_list Fun.id tokens in 109 | match tokens with 110 | | [] -> failwith "Empty ruleset!" 111 | | main :: tokens -> 112 | let settings = main |> get_dict |> find_exn "settings" |> get_dict in 113 | { 114 | tokens = List.filter_map token_of_plist tokens; 115 | background = 116 | Option.map validate_color_exn (List.assoc_opt "background" settings); 117 | foreground = 118 | Option.map validate_color_exn (List.assoc_opt "foreground" settings); 119 | } 120 | 121 | let prefix_length scope selector = 122 | let rec loop acc scope selector = 123 | match (scope, selector) with 124 | | [], _ :: _ -> None (* Selector is more specific than the scope *) 125 | | [], [] -> Some acc 126 | | x :: xs, y :: ys when x = y -> loop (acc + 1) xs ys 127 | | _ :: _, _ -> Some acc 128 | in 129 | loop 0 scope selector 130 | 131 | let rec score_selector scopes_stack (sels : scope_stack) = 132 | (* TextMate's scoring system is arcane and the documentation does not give 133 | a complete specification: 134 | 135 | - https://macromates.com/manual/en/scope_selectors 136 | - http://textmate.1073791.n5.nabble.com/formal-definition-of-scope-selector-syntax-td12109.html 137 | - https://macromates.com/blog/2005/introduction-to-scopes/ 138 | 139 | This specification just adds up all the depths. *) 140 | match (scopes_stack, sels) with 141 | | [], _ :: _ -> None 142 | | scopes :: scopes_stack, sel :: sels -> 143 | Option.bind (prefix_length scopes sel) (fun len -> 144 | Option.map (( + ) len) (score_selector scopes_stack sels)) 145 | | _, [] -> Some 0 146 | 147 | let score_token scopes_stack (token : token) = 148 | let f acc next = 149 | match (acc, score_selector scopes_stack next.select) with 150 | | None, None -> None 151 | | None, Some score -> Some score 152 | | Some _, None -> acc 153 | | Some score1, Some score2 -> 154 | if score1 > score2 then Some score1 else Some score2 155 | in 156 | List.fold_left f None token.selectors 157 | 158 | let style_of_token (token : token) = 159 | let color = 160 | match token.foreground with 161 | | None -> "" 162 | | Some foreground -> "color: " ^ foreground ^ ";" 163 | in 164 | let background = 165 | match token.background with 166 | | None -> "" 167 | | Some background -> "background: " ^ background ^ ";" 168 | in 169 | let style = if token.is_italics then "font-style: italic;" else "" in 170 | let weight = if token.is_bold then "font-weight: bold;" else "" in 171 | let decoration = 172 | if token.is_underline then "text-decoration: underline;" else "" 173 | in 174 | color ^ background ^ style ^ weight ^ decoration 175 | 176 | let create_signals theme scopes i j line : Markup.signal list = 177 | assert (j > i); 178 | let inner_text = String.sub line i (j - i) in 179 | let scopes = List.map (String.split_on_char '.') scopes in 180 | let token = 181 | List.fold_left 182 | (fun acc next -> 183 | let new_score = score_token scopes next in 184 | match acc with 185 | | None -> ( 186 | match new_score with 187 | | None -> None 188 | | Some _ -> Some next) 189 | | Some old -> ( 190 | let old_score = score_token scopes old in 191 | match (old_score, new_score) with 192 | | None, None -> None 193 | | None, Some _ -> Some next 194 | | Some _, None -> Some old 195 | | Some score1, Some score2 -> 196 | if score1 < score2 then Some next else Some old)) 197 | None theme.tokens 198 | in 199 | match token with 200 | | Some token -> 201 | [ 202 | `Start_element (("", "span"), [ (("", "style"), style_of_token token) ]); 203 | `Text [ inner_text ]; 204 | `End_element; 205 | ] 206 | | None -> [ `Text [ inner_text ] ] 207 | 208 | let rec highlight_tokens theme i rev line = function 209 | | [] -> rev 210 | | tok :: toks -> 211 | let j = TmLanguage.ending tok in 212 | let signals = create_signals theme (TmLanguage.scopes tok) i j line in 213 | highlight_tokens theme j (List.rev_append signals rev) line toks 214 | 215 | let rec highlight_lines langs grammar theme stack rev lines = 216 | match lines with 217 | | [] -> rev 218 | | line :: lines -> 219 | let tokens, stack = TmLanguage.tokenize_exn langs grammar stack line in 220 | let rev = highlight_tokens theme 0 (`End_element :: rev) line tokens in 221 | let rev = 222 | `Start_element (("", "span"), [ (("", "class"), "sourceLine") ]) :: rev 223 | in 224 | highlight_lines langs grammar theme stack rev lines 225 | 226 | (* Splits a string into lines, keeping the newline at the end. Assumes that 227 | the string ends with a newline. *) 228 | let lines s = 229 | let rec loop lines i = 230 | match String.index_from_opt s i '\n' with 231 | | None -> List.rev lines 232 | | Some j -> loop (String.sub s i (j - i + 1) :: lines) (j + 1) 233 | in 234 | loop [] 0 235 | 236 | (* Applies a theme to a list of spans. *) 237 | let theme_spans theme = 238 | let color = 239 | match theme.foreground with 240 | | None -> "" 241 | | Some color -> "color: " ^ color ^ ";" 242 | in 243 | let background = 244 | match theme.background with 245 | | None -> "" 246 | | Some background -> "background: " ^ background ^ ";" 247 | in 248 | let style = color ^ background in 249 | [ 250 | `Start_element (("", "code"), []); 251 | `Start_element (("", "pre"), [ (("", "style"), style) ]); 252 | ] 253 | 254 | (* Highlights a block of code. *) 255 | let highlight_block langs grammar theme lines = 256 | let lines = List.map (fun line -> line ^ "\n") lines in 257 | let spans = 258 | try 259 | highlight_lines langs grammar theme TmLanguage.empty (theme_spans theme) 260 | lines 261 | with 262 | | Oniguruma.Error s -> failwith s 263 | | TmLanguage.Error s -> failwith s 264 | in 265 | List.rev (`End_element :: `End_element :: spans) 266 | -------------------------------------------------------------------------------- /src/build.ml: -------------------------------------------------------------------------------- 1 | module Otoml = Otoml_impl.T 2 | open Jingoo 3 | 4 | type name = Index | Name of string 5 | 6 | type page = { frontmatter : Otoml.t; content : string } 7 | and item = Bin of string | Dir of dir | Page of page 8 | and dir = { dir_page : page option; children : (string, item) Hashtbl.t } 9 | 10 | type taxonomy = { 11 | layout : string; 12 | items : (string, Jg_types.tvalue list) Hashtbl.t; 13 | } 14 | 15 | type t = { 16 | config : Config.t; 17 | langs : TmLanguage.t; 18 | taxonomies : (string, taxonomy) Hashtbl.t; 19 | tm_theme : Highlight.theme option; 20 | agda_links : (string list, string list) Hashtbl.t; 21 | } 22 | 23 | let rec jingoo_of_tomlvalue = function 24 | | Otoml.TomlBoolean b -> Jg_types.Tbool b 25 | | Otoml.TomlInteger i -> Jg_types.Tint i 26 | | Otoml.TomlFloat f -> Jg_types.Tfloat f 27 | | Otoml.TomlString s -> Jg_types.Tstr s 28 | | Otoml.TomlLocalDateTime d 29 | | Otoml.TomlOffsetDateTime d 30 | | Otoml.TomlLocalDate d 31 | | Otoml.TomlLocalTime d -> 32 | Jg_types.Tfloat d 33 | | Otoml.TomlArray a | Otoml.TomlTableArray a -> 34 | Jg_types.Tlist (List.map jingoo_of_tomlvalue a) 35 | | Otoml.TomlTable t | Otoml.TomlInlineTable t -> 36 | Jg_types.Tobj (List.map (fun (k, v) -> (k, jingoo_of_tomlvalue v)) t) 37 | 38 | let jingoo_of_page url page = 39 | Jg_types.Tobj 40 | [ 41 | ("url", Jg_types.Tstr url); 42 | ("frontmatter", jingoo_of_tomlvalue page.frontmatter); 43 | ] 44 | 45 | (* Add the Jingoo data to the taxonomy under the specified name. *) 46 | let add_tag t taxonomy name data = 47 | match Hashtbl.find_opt t.taxonomies taxonomy with 48 | | None -> failwith ("Taxonomy " ^ taxonomy ^ " not defined") 49 | | Some taxonomy -> ( 50 | let name = Slug.slugify name in 51 | match Hashtbl.find_opt taxonomy.items name with 52 | | None -> Hashtbl.add taxonomy.items name [ data ] 53 | | Some items -> Hashtbl.replace taxonomy.items name (data :: items)) 54 | 55 | let add_taxonomies t url page = 56 | match Otoml.find_opt page.frontmatter Otoml.get_table [ "taxonomies" ] with 57 | | None -> () 58 | | Some taxonomies -> 59 | taxonomies 60 | |> List.iter (fun (taxonomy, v) -> 61 | match Otoml.get_array Otoml.get_string v with 62 | | exception Otoml.Type_error _ -> 63 | failwith "Expected an array of strings" 64 | | tags -> 65 | tags 66 | |> List.iter (fun tag -> 67 | add_tag t taxonomy tag (jingoo_of_page url page))) 68 | 69 | (* Try to parse TOML frontmatter from the channel. *) 70 | let parse_frontmatter chan = 71 | try 72 | let line = input_line chan in 73 | if line = "+++" then ( 74 | let buf = Buffer.create 100 in 75 | let rec loop () = 76 | let line = input_line chan in 77 | if line = "+++" then () 78 | else ( 79 | Buffer.add_string buf line; 80 | Buffer.add_char buf '\n'; 81 | loop ()) 82 | in 83 | loop (); 84 | match Otoml.Parser.from_string_result (Buffer.contents buf) with 85 | | Ok toml -> toml 86 | | Error e -> failwith e) 87 | else failwith "Missing ending frontmatter!" 88 | with End_of_file -> failwith "Missing frontmatter!" 89 | 90 | let find_grammar t lang = 91 | match TmLanguage.find_by_name t.langs lang with 92 | | Some grammar -> Some grammar 93 | | None -> TmLanguage.find_by_filetype t.langs lang 94 | 95 | (* Highlight the code blocks. *) 96 | let highlight t theme _mapper block = 97 | let highlight_helper grammar code = 98 | Highlight.highlight_block t.langs grammar theme code 99 | |> Markup.of_list |> Markup.write_html |> Markup.to_string 100 | in 101 | match block with 102 | | Cmarkit.Block.Code_block (code_block, meta) -> ( 103 | let lang = Cmarkit.Block.Code_block.info_string code_block in 104 | match lang with 105 | | None -> `Default 106 | | Some (lang, _) -> ( 107 | match find_grammar t lang with 108 | | None -> 109 | prerr_endline ("Warning: unknown language " ^ lang); 110 | `Default 111 | | Some grammar -> 112 | let code = Cmarkit.Block.Code_block.code code_block in 113 | let lines = List.map (fun (fst, _) -> fst) code in 114 | let raw_html = highlight_helper grammar lines in 115 | let block_lines = Cmarkit.Block_line.list_of_string ~meta raw_html in 116 | `Map (Some (Cmarkit.Block.Html_block (block_lines, meta))))) 117 | | _ -> `Default 118 | 119 | let process_md t chan = 120 | let transform = 121 | match t.tm_theme with 122 | | Some theme -> 123 | let mapper = Cmarkit.Mapper.make ~block:(highlight t theme) () in 124 | Cmarkit.Mapper.map_doc mapper 125 | | None -> Fun.id 126 | in 127 | chan |> In_channel.input_all 128 | |> Cmarkit.Doc.of_string ~strict:false 129 | |> transform 130 | |> Cmarkit_html.of_doc ~safe:false 131 | 132 | (* Intercepts [Failure _] exceptions to report the file name. *) 133 | let with_in_smart f path = 134 | try In_channel.with_open_text path f 135 | with Failure e -> failwith (path ^ ": " ^ e) 136 | 137 | let get_agda_module_name line = 138 | let open Angstrom in 139 | let whitespace = 140 | take_while1 (function 141 | | ' ' | '\n' | '\t' | '\r' -> true 142 | | _ -> false) 143 | in 144 | let name_part = 145 | take_while1 (function 146 | | '.' | ';' | '{' | '}' | '(' | ')' | '@' | '"' | ' ' | '\n' | '\t' 147 | | '\r' -> 148 | false 149 | | _ -> true) 150 | in 151 | let qualified_name = sep_by (char '.') name_part in 152 | let main = 153 | string "module" *> whitespace *> qualified_name 154 | <* whitespace <* string "where" 155 | in 156 | parse_string ~consume:Consume.Prefix main line 157 | 158 | let source_dir dir = 159 | Filename.concat "content" (List.fold_left (Fun.flip Filename.concat) "" dir) 160 | 161 | let dispatch t dir name = 162 | let read_path = Filename.concat (source_dir dir) name in 163 | match String.split_on_char '.' name with 164 | | [ "index"; "html" ] -> 165 | read_path 166 | |> with_in_smart (fun chan -> 167 | let frontmatter = parse_frontmatter chan in 168 | (Index, Page { frontmatter; content = In_channel.input_all chan })) 169 | | [ "index"; "md" ] -> 170 | read_path 171 | |> with_in_smart (fun chan -> 172 | let frontmatter = parse_frontmatter chan in 173 | (Index, Page { frontmatter; content = process_md t chan })) 174 | | [ name; "lagda"; "md" ] -> 175 | let module_name = 176 | read_path 177 | |> with_in_smart (fun chan -> 178 | let rec loop () = 179 | match get_agda_module_name (input_line chan) with 180 | | Ok name -> name 181 | | Error _ -> loop () 182 | in 183 | try loop () with End_of_file -> failwith "No module name found!") 184 | in 185 | let exit_code = 186 | Sys.command 187 | (Filename.quote_command "agda" 188 | [ 189 | "--html"; 190 | "--html-highlight=auto"; 191 | "--html-dir=" ^ Config.agda_dest t.config; 192 | read_path; 193 | ]) 194 | in 195 | if exit_code = 0 then ( 196 | Hashtbl.replace t.agda_links module_name (name :: dir); 197 | let generated_md_name = 198 | Filename.concat 199 | (Config.agda_dest t.config) 200 | (String.concat "." module_name) 201 | ^ ".md" 202 | in 203 | let page = 204 | generated_md_name 205 | |> with_in_smart (fun chan -> 206 | let frontmatter = parse_frontmatter chan in 207 | let content = process_md t chan in 208 | { frontmatter; content }) 209 | in 210 | Sys.remove generated_md_name; 211 | (Name name, Page page)) 212 | else failwith ("Agda exited with code " ^ Int.to_string exit_code ^ "!") 213 | | [ name; "html" ] -> 214 | read_path 215 | |> with_in_smart (fun chan -> 216 | let frontmatter = parse_frontmatter chan in 217 | (Name name, Page { frontmatter; content = process_md t chan })) 218 | | [ name; "md" ] -> 219 | read_path 220 | |> with_in_smart (fun chan -> 221 | let frontmatter = parse_frontmatter chan in 222 | (Name name, Page { frontmatter; content = process_md t chan })) 223 | | _ -> 224 | In_channel.with_open_bin read_path (fun chan -> 225 | (Name name, Bin (In_channel.input_all chan))) 226 | 227 | let map_attr p f = 228 | let rec loop p f acc = function 229 | | [] -> List.rev acc 230 | | ((ns, key), link) :: attrs when p key -> 231 | loop p f (((ns, key), f link) :: acc) attrs 232 | | attr :: attrs -> loop p f (attr :: acc) attrs 233 | in 234 | loop p f [] 235 | 236 | let correct_agda_urls t signals = 237 | let has_agda_class = 238 | List.exists (function 239 | | (_, "class"), "Agda" -> true 240 | | _ -> false) 241 | in 242 | let map_href = map_attr (( = ) "href") in 243 | Markup.transform 244 | (fun in_agda signal -> 245 | match signal with 246 | | `Start_element ((_, "pre"), attrs) when has_agda_class attrs -> 247 | ([ signal ], Some (Some 0)) 248 | | `Start_element ((ns, "a"), attrs) -> ( 249 | match in_agda with 250 | | None -> ([ signal ], Some None) 251 | | Some n -> 252 | let attrs = 253 | map_href 254 | (fun link -> 255 | let rec loop acc = function 256 | | [] -> failwith "Unreachable: Empty Agda link" 257 | | [ ext ] -> (acc, ext) 258 | | x :: xs -> loop (x :: acc) xs 259 | in 260 | let module_path, ext = 261 | loop [] (String.split_on_char '.' link) 262 | in 263 | match Hashtbl.find_opt t.agda_links (List.rev module_path) with 264 | | Some dir_path -> 265 | (* The link is to an internal module *) 266 | "/" ^ String.concat "/" (List.rev dir_path) ^ "." ^ ext 267 | | None -> 268 | (* The link is to an external module *) 269 | "/" ^ Filename.concat t.config.Config.agda_dir link) 270 | attrs 271 | in 272 | ([ `Start_element ((ns, "a"), attrs) ], Some (Some (n + 1)))) 273 | | `End_element -> 274 | ( [ signal ], 275 | Some 276 | (match in_agda with 277 | | Some 0 -> None 278 | | Some n -> Some (n - 1) 279 | | None -> None) ) 280 | | signal -> ([ signal ], Some in_agda)) 281 | None signals 282 | 283 | let render_from_file models url path = 284 | let env = 285 | { 286 | Jg_types.std_env with 287 | autoescape = false; 288 | strict_mode = true; 289 | template_dirs = [ "includes" ]; 290 | filters = 291 | [ 292 | ( "format_date", 293 | Jg_types.func_arg2_no_kw (fun format date -> 294 | let open CalendarLib in 295 | Jg_types.Tstr 296 | (Printer.Date.sprint 297 | (Jg_types.unbox_string format) 298 | (Date.from_unixfloat (Jg_types.unbox_float date)))) ); 299 | ( "slugify", 300 | Jg_types.func_arg1_no_kw (fun str -> 301 | Jg_types.Tstr (Slug.slugify (Jg_types.unbox_string str))) ); 302 | ]; 303 | } 304 | in 305 | let path = Filename.concat "layouts" path in 306 | let print_err e = failwith (path ^ ": " ^ url ^ ":" ^ e) in 307 | try Jg_template.from_file ~env ~models path with 308 | | Failure e -> failwith (print_err e) 309 | | Invalid_argument e -> failwith (print_err e) 310 | | Jingoo.Jg_types.SyntaxError e -> failwith (print_err e) 311 | 312 | let render_page pages url page = 313 | match Otoml.find_opt page.frontmatter Otoml.get_string [ "layout" ] with 314 | | None -> page.content 315 | | Some path -> 316 | let models = 317 | [ 318 | ("content", Jg_types.Tstr page.content); 319 | ("pages", Jg_types.Tlist pages); 320 | ("frontmatter", jingoo_of_tomlvalue page.frontmatter); 321 | ] 322 | in 323 | render_from_file models url path 324 | 325 | let relativize_urls url = 326 | Markup.map (function 327 | | `Start_element (name, attrs) -> 328 | `Start_element 329 | ( name, 330 | map_attr 331 | (fun name -> name = "href" || name = "src") 332 | (fun link -> Url.relativize ~src:url ~dest:link) 333 | attrs ) 334 | | signal -> signal) 335 | 336 | let pipeline t url content = 337 | content |> Markup.string |> Markup.parse_html |> Markup.signals 338 | |> correct_agda_urls t |> relativize_urls url |> Markup.write_html 339 | |> Markup.to_string 340 | 341 | let compile_page t siblings path url page = 342 | let content = render_page siblings url page in 343 | add_taxonomies t url page; 344 | let output = pipeline t url content in 345 | Out_channel.with_open_text path (fun out_chan -> 346 | output_string out_chan output) 347 | 348 | let rec load_dir t dir = 349 | let read_dir = source_dir dir in 350 | let files = Sys.readdir read_dir in 351 | let pages = Hashtbl.create (Array.length files) in 352 | let index = 353 | Array.fold_left 354 | (fun index name -> 355 | let path = Filename.concat read_dir name in 356 | if List.exists (Fun.flip Re.execp path) t.config.Config.exclude then 357 | index 358 | else if Sys.is_directory path then 359 | let dir = load_dir t (name :: dir) in 360 | if Hashtbl.mem pages name then 361 | failwith ("Duplicate page " ^ path ^ "!") 362 | else ( 363 | Hashtbl.add pages name (Dir dir); 364 | index) 365 | else 366 | let name, data = dispatch t dir name in 367 | match (index, name) with 368 | | _, Name name -> 369 | if Hashtbl.mem pages name then 370 | failwith ("Duplicate page " ^ path ^ "!") 371 | else ( 372 | Hashtbl.add pages name data; 373 | index) 374 | | Some _, Index -> failwith ("Duplicate index " ^ path ^ "!") 375 | | None, Index -> ( 376 | match data with 377 | | Bin _ -> failwith ("Index is a binary: " ^ path ^ "!") 378 | | Dir _ -> failwith ("Index is a directory: " ^ path ^ "!") 379 | | Page page -> Some page)) 380 | None files 381 | in 382 | { dir_page = index; children = pages } 383 | 384 | let rec compile_dir t root url_prefix { dir_page; children } = 385 | let pages = 386 | Hashtbl.to_seq children 387 | |> Seq.filter_map (function 388 | | name, Page page -> 389 | Some (jingoo_of_page (url_prefix ^ name ^ ".html") page) 390 | | _, _ -> None) 391 | |> List.of_seq 392 | in 393 | Filesystem.touch_dir root; 394 | children 395 | |> Hashtbl.iter (fun name item -> 396 | match item with 397 | | Bin data -> 398 | let dest = Filename.concat root name in 399 | Out_channel.with_open_bin dest (Fun.flip output_string data) 400 | | Dir subdir -> 401 | compile_dir t 402 | (Filename.concat root name) 403 | (url_prefix ^ name ^ "/") 404 | subdir 405 | | Page page -> 406 | let dest = Filename.concat root name ^ ".html" in 407 | compile_page t pages dest (url_prefix ^ name ^ ".html") page); 408 | match dir_page with 409 | | None -> () 410 | | Some page -> 411 | compile_page t pages (Filename.concat root "index.html") url_prefix page 412 | 413 | let build_taxonomy t name taxonomy = 414 | let dir = Filename.concat t.config.Config.dest_dir name in 415 | Filesystem.touch_dir dir; 416 | taxonomy.items 417 | |> Hashtbl.iter (fun slugified_tag pages -> 418 | let output_path = Filename.concat dir slugified_tag ^ ".html" in 419 | let content = 420 | render_from_file 421 | [ 422 | ("pages", Jg_types.Tlist pages); 423 | ("name", Jg_types.Tstr slugified_tag); 424 | ] 425 | output_path taxonomy.layout 426 | in 427 | let url = "/" ^ name ^ "/" ^ slugified_tag ^ ".html" in 428 | let output = pipeline t url content in 429 | Out_channel.with_open_text output_path (fun out_chan -> 430 | output_string out_chan output)) 431 | 432 | let build_taxonomies t = Hashtbl.iter (build_taxonomy t) t.taxonomies 433 | 434 | let build_with_config config = 435 | Filesystem.touch_dir config.Config.dest_dir; 436 | let langs = TmLanguage.create () in 437 | (match Sys.readdir "grammars" with 438 | | exception Sys_error _ -> () 439 | | names -> 440 | names 441 | |> Array.iter (fun name -> 442 | if Sys.is_directory (Filename.concat "grammars" name) then () 443 | else 444 | let path = Filename.concat "grammars" name in 445 | try 446 | let lang = 447 | path 448 | |> with_in_smart (fun chan -> 449 | match Filename.extension name with 450 | | ".plist" | ".tmLanguage" -> 451 | chan |> Plist_xml.from_channel 452 | |> TmLanguage.of_plist_exn 453 | | ".json" -> 454 | chan |> Ezjsonm.from_channel 455 | |> TmLanguage.of_ezjsonm_exn 456 | | ".yaml" | ".YAML-tmLanguage" -> 457 | chan |> In_channel.input_all |> Yaml.of_string_exn 458 | |> TmLanguage.of_ezjsonm_exn 459 | | ext -> 460 | failwith 461 | ("Unsupported syntax extension " ^ ext 462 | ^ ": .plist / .tmLanguage, .json, " 463 | ^ "and .yaml / .YAML-tmLanguage are supported")) 464 | in 465 | TmLanguage.add_grammar langs lang 466 | with 467 | | Oniguruma.Error s -> failwith (path ^ ": Oniguruma: " ^ s) 468 | | Plist_xml.Error (_, e) -> 469 | failwith (path ^ ": " ^ Plist_xml.error_message e) 470 | | Ezjsonm.Parse_error (_, s) -> failwith (path ^ ": " ^ s) 471 | | Invalid_argument s -> failwith (path ^ ": " ^ s) 472 | | TmLanguage.Error s -> failwith (path ^ ": " ^ s))); 473 | let tm_theme = 474 | if Sys.file_exists "theme.tmTheme" then 475 | Some 476 | ("theme.tmTheme" 477 | |> with_in_smart (fun chan -> 478 | Plist_xml.from_channel chan |> Highlight.theme_of_plist)) 479 | else None 480 | in 481 | let t = 482 | { 483 | config; 484 | langs; 485 | taxonomies = Hashtbl.create 2; 486 | tm_theme; 487 | agda_links = Hashtbl.create 29; 488 | } 489 | in 490 | Filesystem.remove_dir t.config.Config.dest_dir; 491 | config.Config.taxonomies 492 | |> List.iter (fun taxonomy -> 493 | Hashtbl.add t.taxonomies taxonomy.Config.name 494 | { layout = taxonomy.Config.layout; items = Hashtbl.create 11 }); 495 | let dir = load_dir t [] in 496 | compile_dir t t.config.Config.dest_dir "/" dir; 497 | build_taxonomies t 498 | 499 | let build () = Config.with_config build_with_config 500 | -------------------------------------------------------------------------------- /web/grammars/TOML.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | toml 8 | 9 | keyEquivalent 10 | ^~T 11 | name 12 | TOML 13 | patterns 14 | 15 | 16 | include 17 | #comments 18 | 19 | 20 | include 21 | #groups 22 | 23 | 24 | include 25 | #key_pair 26 | 27 | 28 | include 29 | #invalid 30 | 31 | 32 | repository 33 | 34 | comments 35 | 36 | begin 37 | (^[ \t]+)?(?=#) 38 | beginCaptures 39 | 40 | 1 41 | 42 | name 43 | punctuation.whitespace.comment.leading.toml 44 | 45 | 46 | end 47 | (?!\G) 48 | patterns 49 | 50 | 51 | begin 52 | # 53 | beginCaptures 54 | 55 | 0 56 | 57 | name 58 | punctuation.definition.comment.toml 59 | 60 | 61 | end 62 | \n 63 | name 64 | comment.line.number-sign.toml 65 | 66 | 67 | 68 | groups 69 | 70 | patterns 71 | 72 | 73 | captures 74 | 75 | 1 76 | 77 | name 78 | punctuation.definition.section.begin.toml 79 | 80 | 2 81 | 82 | patterns 83 | 84 | 85 | match 86 | [^\s.]+ 87 | name 88 | entity.name.section.toml 89 | 90 | 91 | 92 | 3 93 | 94 | name 95 | punctuation.definition.section.begin.toml 96 | 97 | 98 | match 99 | ^\s*(\[)([^\[\]]*)(\]) 100 | name 101 | meta.group.toml 102 | 103 | 104 | captures 105 | 106 | 1 107 | 108 | name 109 | punctuation.definition.section.begin.toml 110 | 111 | 2 112 | 113 | patterns 114 | 115 | 116 | match 117 | [^\s.]+ 118 | name 119 | entity.name.section.toml 120 | 121 | 122 | 123 | 3 124 | 125 | name 126 | punctuation.definition.section.begin.toml 127 | 128 | 129 | match 130 | ^\s*(\[\[)([^\[\]]*)(\]\]) 131 | name 132 | meta.group.double.toml 133 | 134 | 135 | 136 | invalid 137 | 138 | match 139 | \S+(\s*(?=\S))? 140 | name 141 | invalid.illegal.not-allowed-here.toml 142 | 143 | key_pair 144 | 145 | patterns 146 | 147 | 148 | begin 149 | ([A-Za-z0-9_-]+)\s*(=)\s* 150 | captures 151 | 152 | 1 153 | 154 | name 155 | variable.other.key.toml 156 | 157 | 2 158 | 159 | name 160 | punctuation.separator.key-value.toml 161 | 162 | 163 | end 164 | (?<=\S)(?<!=)|$ 165 | patterns 166 | 167 | 168 | include 169 | #primatives 170 | 171 | 172 | 173 | 174 | begin 175 | ((")(.*?)("))\s*(=)\s* 176 | captures 177 | 178 | 1 179 | 180 | name 181 | variable.other.key.toml 182 | 183 | 2 184 | 185 | name 186 | punctuation.definition.variable.begin.toml 187 | 188 | 3 189 | 190 | patterns 191 | 192 | 193 | match 194 | \\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}) 195 | name 196 | constant.character.escape.toml 197 | 198 | 199 | match 200 | \\[^btnfr"\\] 201 | name 202 | invalid.illegal.escape.toml 203 | 204 | 205 | match 206 | " 207 | name 208 | invalid.illegal.not-allowed-here.toml 209 | 210 | 211 | 212 | 4 213 | 214 | name 215 | punctuation.definition.variable.end.toml 216 | 217 | 5 218 | 219 | name 220 | punctuation.separator.key-value.toml 221 | 222 | 223 | end 224 | (?<=\S)(?<!=)|$ 225 | patterns 226 | 227 | 228 | include 229 | #primatives 230 | 231 | 232 | 233 | 234 | begin 235 | ((')([^']*)('))\s*(=)\s* 236 | captures 237 | 238 | 1 239 | 240 | name 241 | variable.other.key.toml 242 | 243 | 2 244 | 245 | name 246 | punctuation.definition.variable.begin.toml 247 | 248 | 4 249 | 250 | name 251 | punctuation.definition.variable.end.toml 252 | 253 | 5 254 | 255 | name 256 | punctuation.separator.key-value.toml 257 | 258 | 259 | end 260 | (?<=\S)(?<!=)|$ 261 | patterns 262 | 263 | 264 | include 265 | #primatives 266 | 267 | 268 | 269 | 270 | begin 271 | (?x) 272 | ( 273 | ( 274 | (?: 275 | [A-Za-z0-9_-]+ # Bare key 276 | | " (?:[^"\\]|\\.)* " # Double quoted key 277 | | ' [^']* ' # Sindle quoted key 278 | ) 279 | (?: 280 | \s* \. \s* # Dot 281 | | (?= \s* =) # or look-ahead for equals 282 | ) 283 | ){2,} # Ensure at least one dot 284 | ) 285 | \s*(=)\s* 286 | 287 | captures 288 | 289 | 1 290 | 291 | name 292 | variable.other.key.toml 293 | patterns 294 | 295 | 296 | match 297 | \. 298 | name 299 | punctuation.separator.variable.toml 300 | 301 | 302 | captures 303 | 304 | 1 305 | 306 | name 307 | punctuation.definition.variable.begin.toml 308 | 309 | 2 310 | 311 | patterns 312 | 313 | 314 | match 315 | \\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}) 316 | name 317 | constant.character.escape.toml 318 | 319 | 320 | match 321 | \\[^btnfr"\\] 322 | name 323 | invalid.illegal.escape.toml 324 | 325 | 326 | 327 | 3 328 | 329 | name 330 | punctuation.definition.variable.end.toml 331 | 332 | 333 | match 334 | (")((?:[^"\\]|\\.)*)(") 335 | 336 | 337 | captures 338 | 339 | 1 340 | 341 | name 342 | punctuation.definition.variable.begin.toml 343 | 344 | 2 345 | 346 | name 347 | punctuation.definition.variable.end.toml 348 | 349 | 350 | match 351 | (')[^']*(') 352 | 353 | 354 | 355 | 3 356 | 357 | name 358 | punctuation.separator.key-value.toml 359 | 360 | 361 | comment 362 | Dotted key 363 | end 364 | (?<=\S)(?<!=)|$ 365 | patterns 366 | 367 | 368 | include 369 | #primatives 370 | 371 | 372 | 373 | 374 | 375 | primatives 376 | 377 | patterns 378 | 379 | 380 | begin 381 | \G""" 382 | beginCaptures 383 | 384 | 0 385 | 386 | name 387 | punctuation.definition.string.begin.toml 388 | 389 | 390 | end 391 | "{3,5} 392 | endCaptures 393 | 394 | 0 395 | 396 | name 397 | punctuation.definition.string.end.toml 398 | 399 | 400 | name 401 | string.quoted.triple.double.toml 402 | patterns 403 | 404 | 405 | match 406 | \\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}) 407 | name 408 | constant.character.escape.toml 409 | 410 | 411 | match 412 | \\[^btnfr"\\\n] 413 | name 414 | invalid.illegal.escape.toml 415 | 416 | 417 | 418 | 419 | begin 420 | \G" 421 | beginCaptures 422 | 423 | 0 424 | 425 | name 426 | punctuation.definition.string.begin.toml 427 | 428 | 429 | end 430 | " 431 | endCaptures 432 | 433 | 0 434 | 435 | name 436 | punctuation.definition.string.end.toml 437 | 438 | 439 | name 440 | string.quoted.double.toml 441 | patterns 442 | 443 | 444 | match 445 | \\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}) 446 | name 447 | constant.character.escape.toml 448 | 449 | 450 | match 451 | \\[^btnfr"\\] 452 | name 453 | invalid.illegal.escape.toml 454 | 455 | 456 | 457 | 458 | begin 459 | \G''' 460 | beginCaptures 461 | 462 | 0 463 | 464 | name 465 | punctuation.definition.string.begin.toml 466 | 467 | 468 | end 469 | '{3,5} 470 | endCaptures 471 | 472 | 0 473 | 474 | name 475 | punctuation.definition.string.end.toml 476 | 477 | 478 | name 479 | string.quoted.triple.single.toml 480 | 481 | 482 | begin 483 | \G' 484 | beginCaptures 485 | 486 | 0 487 | 488 | name 489 | punctuation.definition.string.begin.toml 490 | 491 | 492 | end 493 | ' 494 | endCaptures 495 | 496 | 0 497 | 498 | name 499 | punctuation.definition.string.end.toml 500 | 501 | 502 | name 503 | string.quoted.single.toml 504 | 505 | 506 | match 507 | \G(?x) 508 | [0-9]{4} 509 | - 510 | (0[1-9]|1[012]) 511 | - 512 | (?!00|3[2-9])[0-3][0-9] 513 | ( 514 | [Tt ] 515 | (?!2[5-9])[0-2][0-9] 516 | : 517 | [0-5][0-9] 518 | : 519 | (?!6[1-9])[0-6][0-9] 520 | (\.[0-9]+)? 521 | ( 522 | Z 523 | | [+-](?!2[5-9])[0-2][0-9]:[0-5][0-9] 524 | )? 525 | )? 526 | 527 | name 528 | constant.other.date.toml 529 | 530 | 531 | match 532 | \G(?x) 533 | (?!2[5-9])[0-2][0-9] 534 | : 535 | [0-5][0-9] 536 | : 537 | (?!6[1-9])[0-6][0-9] 538 | (\.[0-9]+)? 539 | 540 | name 541 | constant.other.time.toml 542 | 543 | 544 | match 545 | \G(true|false) 546 | name 547 | constant.language.boolean.toml 548 | 549 | 550 | match 551 | \G0x\h(\h|_\h)* 552 | name 553 | constant.numeric.hex.toml 554 | 555 | 556 | match 557 | \G0o[0-7]([0-7]|_[0-7])* 558 | name 559 | constant.numeric.octal.toml 560 | 561 | 562 | match 563 | \G0b[01]([01]|_[01])* 564 | name 565 | constant.numeric.binary.toml 566 | 567 | 568 | match 569 | \G[+-]?(inf|nan) 570 | name 571 | constant.numeric.toml 572 | 573 | 574 | match 575 | (?x) 576 | \G 577 | ( 578 | [+-]? 579 | ( 580 | 0 581 | | ([1-9](([0-9]|_[0-9])+)?) 582 | ) 583 | ) 584 | (?=[.eE]) 585 | ( 586 | \. 587 | ([0-9](([0-9]|_[0-9])+)?) 588 | )? 589 | ( 590 | [eE] 591 | ([+-]?[0-9](([0-9]|_[0-9])+)?) 592 | )? 593 | 594 | name 595 | constant.numeric.float.toml 596 | 597 | 598 | match 599 | (?x) 600 | \G 601 | ( 602 | [+-]? 603 | ( 604 | 0 605 | | ([1-9](([0-9]|_[0-9])+)?) 606 | ) 607 | ) 608 | 609 | name 610 | constant.numeric.integer.toml 611 | 612 | 613 | begin 614 | \G\[ 615 | beginCaptures 616 | 617 | 0 618 | 619 | name 620 | punctuation.definition.array.begin.toml 621 | 622 | 623 | end 624 | \] 625 | endCaptures 626 | 627 | 0 628 | 629 | name 630 | punctuation.definition.array.end.toml 631 | 632 | 633 | name 634 | meta.array.toml 635 | patterns 636 | 637 | 638 | begin 639 | (?=["'']|[+-]?[0-9]|[+-]?(inf|nan)|true|false|\[|\{) 640 | end 641 | ,|(?=]) 642 | endCaptures 643 | 644 | 0 645 | 646 | name 647 | punctuation.separator.array.toml 648 | 649 | 650 | patterns 651 | 652 | 653 | include 654 | #primatives 655 | 656 | 657 | include 658 | #comments 659 | 660 | 661 | include 662 | #invalid 663 | 664 | 665 | 666 | 667 | include 668 | #comments 669 | 670 | 671 | include 672 | #invalid 673 | 674 | 675 | 676 | 677 | begin 678 | \G\{ 679 | beginCaptures 680 | 681 | 0 682 | 683 | name 684 | punctuation.definition.inline-table.begin.toml 685 | 686 | 687 | end 688 | \} 689 | endCaptures 690 | 691 | 0 692 | 693 | name 694 | punctuation.definition.inline-table.end.toml 695 | 696 | 697 | name 698 | meta.inline-table.toml 699 | patterns 700 | 701 | 702 | begin 703 | (?=\S) 704 | end 705 | ,|(?=}) 706 | endCaptures 707 | 708 | 0 709 | 710 | name 711 | punctuation.separator.inline-table.toml 712 | 713 | 714 | patterns 715 | 716 | 717 | include 718 | #key_pair 719 | 720 | 721 | 722 | 723 | include 724 | #comments 725 | 726 | 727 | 728 | 729 | 730 | 731 | scopeName 732 | source.toml 733 | uuid 734 | 7DEF2EDB-5BB7-4DD2-9E78-3541A26B7923 735 | 736 | 737 | -------------------------------------------------------------------------------- /web/theme.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | author 6 | GitHub 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #24292e 14 | foreground 15 | #f6f8fa 16 | lineHighlight 17 | #444d56 18 | invisibles 19 | #6a737d 20 | selection 21 | #4c2889 22 | caret 23 | #fff 24 | diffRenamed 25 | #fafbfc 26 | diffModified 27 | #f9c513 28 | diffDeleted 29 | #d73a49 30 | diffAdded 31 | #34d058 32 | inactiveSelection 33 | #444d56 34 | selectionBorder 35 | #444d56 36 | findHighlight 37 | #fb8532 38 | findHighlightForeground 39 | #24292e 40 | guide 41 | #6a737d 42 | activeGuide 43 | #f6f8fa 44 | stackGuide 45 | #959da5 46 | highlight 47 | #f6f8fa 48 | popupCss 49 | <![CDATA[html { background-color: #444d56; } h1, h2, h3, h4, h5, h6 { color: #0366d6; margin-top: 0.2em; margin-bottom: 0.2em; } h1 { font-size: 1.5em; } h2 { font-size: 1.4em; } h3 { font-size: 1.3em; } h4 { font-size: 1.2em; } h5 { font-size: 1.1em; } h6 { font-size: 1em; } blockquote { color: #c8e1ff; display: block; font-style: italic; } pre { display: block; } a { color: #79b8ff; font-style: underline; } body { color: #f6f8fa; background-color: #24292e; margin: 1px; font-size: 1em; padding: 0.2em; } .danger { color: #d73a49; } .important, .attention { color: #b392f0; } .caution, .warning { color: #fb8532; } .note { color: #fb8532; }]]> 50 | highlightForeground 51 | #f6f8fa 52 | tagsOptions 53 | underline 54 | bracketContentsOptions 55 | underline 56 | bracketContentsForeground 57 | #e1e4e8 58 | bracketsOptions 59 | underline 60 | bracketsForeground 61 | #e1e4e8 62 | gutterForeground 63 | #f6f8fa 64 | 65 | 66 | 67 | scope 68 | comment, punctuation.definition.comment, string.comment 69 | settings 70 | 71 | foreground 72 | #959da5 73 | 74 | name 75 | Comment 76 | 77 | 78 | scope 79 | constant, entity.name.constant, variable.other.constant, variable.language 80 | settings 81 | 82 | foreground 83 | #c8e1ff 84 | 85 | name 86 | Constant 87 | 88 | 89 | scope 90 | keyword.operator.symbole, keyword.other.mark 91 | name 92 | Clojure workaround; don't highlight these separately from their enclosing scope 93 | settings 94 | 95 | 96 | 97 | scope 98 | entity, entity.name 99 | settings 100 | 101 | fontStyle 102 | 103 | foreground 104 | #b392f0 105 | 106 | name 107 | Entity 108 | 109 | 110 | scope 111 | variable.parameter.function 112 | settings 113 | 114 | foreground 115 | #f6f8fa 116 | 117 | 118 | 119 | scope 120 | entity.name.tag 121 | settings 122 | 123 | fontStyle 124 | 125 | foreground 126 | #7bcc72 127 | 128 | 129 | 130 | scope 131 | keyword 132 | settings 133 | 134 | fontStyle 135 | 136 | foreground 137 | #ea4a5a 138 | 139 | name 140 | Keyword 141 | 142 | 143 | scope 144 | storage, storage.type 145 | settings 146 | 147 | foreground 148 | #ea4a5a 149 | 150 | name 151 | Storage 152 | 153 | 154 | scope 155 | storage.modifier.package, storage.modifier.import, storage.type.java 156 | settings 157 | 158 | foreground 159 | #f6f8fa 160 | 161 | 162 | 163 | scope 164 | string, punctuation.definition.string, string punctuation.section.embedded source 165 | settings 166 | 167 | fontStyle 168 | 169 | foreground 170 | #79b8ff 171 | 172 | name 173 | String 174 | 175 | 176 | name 177 | Ada workaround; don't highlight imports as strings 178 | scope 179 | string.unquoted.import.ada 180 | settings 181 | 182 | 183 | 184 | scope 185 | support 186 | settings 187 | 188 | fontStyle 189 | 190 | foreground 191 | #c8e1ff 192 | 193 | name 194 | Support 195 | 196 | 197 | scope 198 | meta.property-name 199 | settings 200 | 201 | fontStyle 202 | 203 | foreground 204 | #c8e1ff 205 | 206 | 207 | 208 | scope 209 | variable 210 | settings 211 | 212 | fontStyle 213 | 214 | foreground 215 | #fb8532 216 | 217 | name 218 | Variable 219 | 220 | 221 | scope 222 | variable.other 223 | settings 224 | 225 | foreground 226 | #f6f8fa 227 | 228 | 229 | 230 | scope 231 | invalid.broken 232 | settings 233 | 234 | fontStyle 235 | bold italic underline 236 | foreground 237 | #d73a49 238 | 239 | name 240 | Invalid - Broken 241 | 242 | 243 | scope 244 | invalid.deprecated 245 | settings 246 | 247 | fontStyle 248 | bold italic underline 249 | foreground 250 | #d73a49 251 | 252 | name 253 | Invalid – Deprecated 254 | 255 | 256 | scope 257 | invalid.illegal 258 | settings 259 | 260 | fontStyle 261 | italic underline 262 | foreground 263 | #fafbfc 264 | background 265 | #d73a49 266 | 267 | name 268 | Invalid – Illegal 269 | 270 | 271 | scope 272 | carriage-return 273 | settings 274 | 275 | fontStyle 276 | italic underline 277 | foreground 278 | #fafbfc 279 | background 280 | #d73a49 281 | content 282 | ^M 283 | 284 | name 285 | Carriage Return 286 | 287 | 288 | scope 289 | invalid.unimplemented 290 | settings 291 | 292 | fontStyle 293 | bold italic underline 294 | foreground 295 | #d73a49 296 | 297 | name 298 | Invalid - Unimplemented 299 | 300 | 301 | scope 302 | message.error 303 | settings 304 | 305 | foreground 306 | #d73a49 307 | 308 | 309 | 310 | scope 311 | string source 312 | settings 313 | 314 | fontStyle 315 | 316 | foreground 317 | #f6f8fa 318 | 319 | name 320 | String embedded-source 321 | 322 | 323 | scope 324 | string variable 325 | settings 326 | 327 | fontStyle 328 | 329 | foreground 330 | #c8e1ff 331 | 332 | name 333 | String variable 334 | 335 | 336 | scope 337 | source.regexp, string.regexp 338 | settings 339 | 340 | fontStyle 341 | 342 | foreground 343 | #79b8ff 344 | 345 | name 346 | String.regexp 347 | 348 | 349 | scope 350 | string.regexp.character-class, string.regexp constant.character.escape, string.regexp source.ruby.embedded, string.regexp string.regexp.arbitrary-repitition 351 | settings 352 | 353 | foreground 354 | #79b8ff 355 | 356 | name 357 | String.regexp.«special» 358 | 359 | 360 | scope 361 | string.regexp constant.character.escape 362 | settings 363 | 364 | fontStyle 365 | bold 366 | foreground 367 | #7bcc72 368 | 369 | name 370 | String.regexp constant.character.escape 371 | 372 | 373 | scope 374 | support.constant 375 | settings 376 | 377 | fontStyle 378 | 379 | foreground 380 | #c8e1ff 381 | 382 | name 383 | Support.constant 384 | 385 | 386 | scope 387 | support.variable 388 | settings 389 | 390 | foreground 391 | #c8e1ff 392 | 393 | name 394 | Support.variable 395 | 396 | 397 | scope 398 | meta.module-reference 399 | settings 400 | 401 | foreground 402 | #c8e1ff 403 | 404 | name 405 | meta module-reference 406 | 407 | 408 | scope 409 | markup.list 410 | settings 411 | 412 | foreground 413 | #fb8532 414 | 415 | name 416 | Markup.list 417 | 418 | 419 | scope 420 | markup.heading, markup.heading entity.name 421 | settings 422 | 423 | fontStyle 424 | bold 425 | foreground 426 | #0366d6 427 | 428 | name 429 | Markup.heading 430 | 431 | 432 | scope 433 | markup.quote 434 | settings 435 | 436 | foreground 437 | #c8e1ff 438 | 439 | name 440 | Markup.quote 441 | 442 | 443 | scope 444 | markup.italic 445 | settings 446 | 447 | fontStyle 448 | italic 449 | foreground 450 | #f6f8fa 451 | 452 | name 453 | Markup.italic 454 | 455 | 456 | scope 457 | markup.bold 458 | settings 459 | 460 | fontStyle 461 | bold 462 | foreground 463 | #f6f8fa 464 | 465 | name 466 | Markup.bold 467 | 468 | 469 | scope 470 | markup.raw 471 | settings 472 | 473 | fontStyle 474 | 475 | foreground 476 | #c8e1ff 477 | 478 | name 479 | Markup.raw 480 | 481 | 482 | scope 483 | markup.deleted, meta.diff.header.from-file, punctuation.definition.deleted 484 | settings 485 | 486 | background 487 | #ffeef0 488 | foreground 489 | #b31d28 490 | 491 | name 492 | Markup.deleted 493 | 494 | 495 | scope 496 | markup.inserted, meta.diff.header.to-file, punctuation.definition.inserted 497 | settings 498 | 499 | background 500 | #f0fff4 501 | foreground 502 | #176f2c 503 | 504 | name 505 | Markup.inserted 506 | 507 | 508 | scope 509 | markup.changed, punctuation.definition.changed 510 | settings 511 | 512 | background 513 | #fffdef 514 | foreground 515 | #b08800 516 | 517 | 518 | 519 | scope 520 | markup.ignored, markup.untracked 521 | settings 522 | 523 | foreground 524 | #2f363d 525 | background 526 | #959da5 527 | 528 | 529 | 530 | scope 531 | meta.diff.range 532 | settings 533 | 534 | fontStyle 535 | bold 536 | foreground 537 | #b392f0 538 | 539 | 540 | 541 | scope 542 | meta.diff.header 543 | settings 544 | 545 | foreground 546 | #c8e1ff 547 | 548 | 549 | 550 | scope 551 | meta.separator 552 | settings 553 | 554 | fontStyle 555 | bold 556 | foreground 557 | #0366d6 558 | 559 | name 560 | Meta.separator 561 | 562 | 563 | name 564 | Output 565 | scope 566 | meta.output 567 | settings 568 | 569 | foreground 570 | #0366d6 571 | 572 | 573 | 574 | scope 575 | brackethighlighter.tag, brackethighlighter.curly, brackethighlighter.round, brackethighlighter.square, brackethighlighter.angle, brackethighlighter.quote 576 | settings 577 | 578 | foreground 579 | #ffeef0 580 | 581 | 582 | 583 | scope 584 | brackethighlighter.unmatched 585 | settings 586 | 587 | foreground 588 | #d73a49 589 | 590 | 591 | 592 | scope 593 | sublimelinter.mark.error 594 | settings 595 | 596 | foreground 597 | #d73a49 598 | 599 | 600 | 601 | scope 602 | sublimelinter.mark.warning 603 | settings 604 | 605 | foreground 606 | #fb8532 607 | 608 | 609 | 610 | scope 611 | sublimelinter.gutter-mark 612 | settings 613 | 614 | foreground 615 | #6a737d 616 | 617 | 618 | 619 | scope 620 | constant.other.reference.link, string.other.link 621 | settings 622 | 623 | foreground 624 | #79b8ff 625 | fontStyle 626 | underline 627 | 628 | 629 | 630 | comment 631 | GitHub Dark syntax theme 632 | name 633 | GitHub Dark 634 | semanticClass 635 | theme.dark.github 636 | filename 637 | github-dark 638 | uuid 639 | C8E24EAE-6212-41E3-AC1A-F49362B6150D 640 | 641 | --------------------------------------------------------------------------------