about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--doc/.gitignore3
-rw-r--r--doc/base/karderio-Toy-baloon.svg161
-rw-r--r--doc/base/pygments-dark.css83
-rw-r--r--doc/base/pygments-light.css73
-rw-r--r--doc/base/style.css173
-rw-r--r--doc/html.xslt63
-rw-r--r--doc/pages/demo/katex/index.xml115
-rw-r--r--doc/pages/demo/pygments/index.xml66
-rw-r--r--doc/pages/index.xml14
-rw-r--r--doc/rss.xslt26
-rwxr-xr-xdoc/rub95
-rw-r--r--doc/shell.nix10
12 files changed, 882 insertions, 0 deletions
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 0000000..e2c5f1f
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1,3 @@
+cache/
+out/
+.doit.db
diff --git a/doc/base/karderio-Toy-baloon.svg b/doc/base/karderio-Toy-baloon.svg
new file mode 100644
index 0000000..5fd9325
--- /dev/null
+++ b/doc/base/karderio-Toy-baloon.svg
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:cc="http://web.resource.org/cc/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/"
+    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+    xmlns:svg="http://www.w3.org/2000/svg"
+    xmlns:ns1="http://sozi.baierouge.fr"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    id="svg2989"
+    sodipodi:docname="baloon.svg"
+    viewBox="0 0 422.04 468.44"
+    sodipodi:version="0.32"
+    version="1.0"
+    inkscape:version="0.44"
+    sodipodi:docbase="/home/ubun/Desktop"
+  >
+  <sodipodi:namedview
+      id="base"
+      bordercolor="#666666"
+      inkscape:pageshadow="2"
+      guidetolerance="10"
+      pagecolor="#ffffff"
+      gridtolerance="10000"
+      inkscape:window-height="598"
+      inkscape:zoom="0.35"
+      objecttolerance="10"
+      borderopacity="1.0"
+      inkscape:current-layer="layer1"
+      inkscape:cx="375"
+      inkscape:cy="520"
+      inkscape:window-y="51"
+      inkscape:window-x="0"
+      inkscape:window-width="946"
+      inkscape:pageopacity="0.0"
+      inkscape:document-units="px"
+  />
+  <g
+      id="layer1"
+      inkscape:label="Layer 1"
+      inkscape:groupmode="layer"
+      transform="translate(-171.84 -272.42)"
+    >
+    <g
+        id="g1989"
+        style="stroke:black;stroke-width:2;fill:#ff7f2a"
+        transform="matrix(3.3799 0 0 3.2813 -1903.1 -2411.3)"
+      >
+      <path
+          id="path1991"
+          style="stroke:black;stroke-width:2;fill:#ff7f2a"
+          d="m653.39 946.79c-160.62-201.21 248.82-137.02 4.55 0.64-2.37 0.8-20.96 6.25-21.08 6.82-1.94 8.83 11.3 3.86 15.1 1.84-1.44 6.53 26.57 2.75 19.12-1.29-5.85-3.17-12.68-3.75-17.69-8.01z"
+      />
+    </g
+    >
+    <g
+        id="g1997"
+        style="fill:white"
+        transform="matrix(3.3799 0 0 3.2813 -1903.1 -2411.3)"
+      >
+      <path
+          id="path1999"
+          style="fill:white"
+          d="m700.19 842.32c4.1-1.86 6.86-3.39 11.83-6.29 4.52 1.81 19.97 17.92 17.62 22.02-1.28 2.24-1.91 2.3-3.07 0.73-6.47-8.81-22.49-17.09-26.38-16.46z"
+      />
+    </g
+    >
+  </g
+  >
+  <metadata
+    >
+    <rdf:RDF
+      >
+      <cc:Work
+        >
+        <dc:format
+          >image/svg+xml</dc:format
+        >
+        <dc:type
+            rdf:resource="http://purl.org/dc/dcmitype/StillImage"
+        />
+        <cc:license
+            rdf:resource="http://creativecommons.org/licenses/publicdomain/"
+        />
+        <dc:publisher
+          >
+          <cc:Agent
+              rdf:about="http://openclipart.org/"
+            >
+            <dc:title
+              >Openclipart</dc:title
+            >
+          </cc:Agent
+          >
+        </dc:publisher
+        >
+        <dc:title
+          >Toy baloon</dc:title
+        >
+        <dc:date
+          >2006-10-21T06:10:16</dc:date
+        >
+        <dc:description
+          >2D orange party baloon</dc:description
+        >
+        <dc:source
+          >https://openclipart.org/detail/808/toy-baloon-by-karderio</dc:source
+        >
+        <dc:creator
+          >
+          <cc:Agent
+            >
+            <dc:title
+              >karderio</dc:title
+            >
+          </cc:Agent
+          >
+        </dc:creator
+        >
+        <dc:subject
+          >
+          <rdf:Bag
+            >
+            <rdf:li
+              >line art</rdf:li
+            >
+            <rdf:li
+              >party baloon</rdf:li
+            >
+            <rdf:li
+              >toy baloon</rdf:li
+            >
+          </rdf:Bag
+          >
+        </dc:subject
+        >
+      </cc:Work
+      >
+      <cc:License
+          rdf:about="http://creativecommons.org/licenses/publicdomain/"
+        >
+        <cc:permits
+            rdf:resource="http://creativecommons.org/ns#Reproduction"
+        />
+        <cc:permits
+            rdf:resource="http://creativecommons.org/ns#Distribution"
+        />
+        <cc:permits
+            rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
+        />
+      </cc:License
+      >
+    </rdf:RDF
+    >
+  </metadata
+  >
+</svg
+>
diff --git a/doc/base/pygments-dark.css b/doc/base/pygments-dark.css
new file mode 100644
index 0000000..2160c0b
--- /dev/null
+++ b/doc/base/pygments-dark.css
@@ -0,0 +1,83 @@
+pre { line-height: 125%; }
+td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+.hll { background-color: #0000ff }
+.c { color: #00ff00 } /* Comment */
+.err { color: #dddddd } /* Error */
+.esc { color: #dddddd } /* Escape */
+.g { color: #dddddd } /* Generic */
+.k { color: #ff0000 } /* Keyword */
+.l { color: #dddddd } /* Literal */
+.n { color: #dddddd } /* Name */
+.o { color: #dddddd } /* Operator */
+.x { color: #dddddd } /* Other */
+.p { color: #dddddd } /* Punctuation */
+.ch { color: #00ff00 } /* Comment.Hashbang */
+.cm { color: #00ff00 } /* Comment.Multiline */
+.cp { color: #e5e5e5 } /* Comment.Preproc */
+.cpf { color: #00ff00 } /* Comment.PreprocFile */
+.c1 { color: #00ff00 } /* Comment.Single */
+.cs { color: #00ff00 } /* Comment.Special */
+.gd { color: #dddddd } /* Generic.Deleted */
+.ge { color: #dddddd } /* Generic.Emph */
+.gr { color: #dddddd } /* Generic.Error */
+.gh { color: #dddddd } /* Generic.Heading */
+.gi { color: #dddddd } /* Generic.Inserted */
+.go { color: #dddddd } /* Generic.Output */
+.gp { color: #dddddd } /* Generic.Prompt */
+.gs { color: #dddddd } /* Generic.Strong */
+.gu { color: #dddddd } /* Generic.Subheading */
+.gt { color: #dddddd } /* Generic.Traceback */
+.kc { color: #ff0000 } /* Keyword.Constant */
+.kd { color: #ff0000 } /* Keyword.Declaration */
+.kn { color: #ff0000 } /* Keyword.Namespace */
+.kp { color: #ff0000 } /* Keyword.Pseudo */
+.kr { color: #ff0000 } /* Keyword.Reserved */
+.kt { color: #ee82ee } /* Keyword.Type */
+.ld { color: #dddddd } /* Literal.Date */
+.m { color: #dddddd } /* Literal.Number */
+.s { color: #87ceeb } /* Literal.String */
+.na { color: #dddddd } /* Name.Attribute */
+.nb { color: #dddddd } /* Name.Builtin */
+.nc { color: #dddddd } /* Name.Class */
+.no { color: #7fffd4 } /* Name.Constant */
+.nd { color: #dddddd } /* Name.Decorator */
+.ni { color: #dddddd } /* Name.Entity */
+.ne { color: #dddddd } /* Name.Exception */
+.nf { color: #ffff00 } /* Name.Function */
+.nl { color: #dddddd } /* Name.Label */
+.nn { color: #dddddd } /* Name.Namespace */
+.nx { color: #dddddd } /* Name.Other */
+.py { color: #dddddd } /* Name.Property */
+.nt { color: #dddddd } /* Name.Tag */
+.nv { color: #eedd82 } /* Name.Variable */
+.ow { color: #dddddd } /* Operator.Word */
+.pm { color: #dddddd } /* Punctuation.Marker */
+.w { color: #dddddd } /* Text.Whitespace */
+.mb { color: #dddddd } /* Literal.Number.Bin */
+.mf { color: #dddddd } /* Literal.Number.Float */
+.mh { color: #dddddd } /* Literal.Number.Hex */
+.mi { color: #dddddd } /* Literal.Number.Integer */
+.mo { color: #dddddd } /* Literal.Number.Oct */
+.sa { color: #87ceeb } /* Literal.String.Affix */
+.sb { color: #87ceeb } /* Literal.String.Backtick */
+.sc { color: #87ceeb } /* Literal.String.Char */
+.dl { color: #87ceeb } /* Literal.String.Delimiter */
+.sd { color: #87ceeb } /* Literal.String.Doc */
+.s2 { color: #87ceeb } /* Literal.String.Double */
+.se { color: #87ceeb } /* Literal.String.Escape */
+.sh { color: #87ceeb } /* Literal.String.Heredoc */
+.si { color: #87ceeb } /* Literal.String.Interpol */
+.sx { color: #87ceeb } /* Literal.String.Other */
+.sr { color: #87ceeb } /* Literal.String.Regex */
+.s1 { color: #87ceeb } /* Literal.String.Single */
+.ss { color: #87ceeb } /* Literal.String.Symbol */
+.bp { color: #dddddd } /* Name.Builtin.Pseudo */
+.fm { color: #ffff00 } /* Name.Function.Magic */
+.vc { color: #eedd82 } /* Name.Variable.Class */
+.vg { color: #eedd82 } /* Name.Variable.Global */
+.vi { color: #eedd82 } /* Name.Variable.Instance */
+.vm { color: #eedd82 } /* Name.Variable.Magic */
+.il { color: #dddddd } /* Literal.Number.Integer.Long */
diff --git a/doc/base/pygments-light.css b/doc/base/pygments-light.css
new file mode 100644
index 0000000..e2cc7b8
--- /dev/null
+++ b/doc/base/pygments-light.css
@@ -0,0 +1,73 @@
+pre { line-height: 125%; }
+td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+.hll { background-color: #ffffcc }
+.c { color: #3D7B7B; font-style: italic } /* Comment */
+.err { border: 1px solid #FF0000 } /* Error */
+.k { color: #008000; font-weight: bold } /* Keyword */
+.o { color: #666666 } /* Operator */
+.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.cp { color: #9C6500 } /* Comment.Preproc */
+.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
+.gd { color: #A00000 } /* Generic.Deleted */
+.ge { font-style: italic } /* Generic.Emph */
+.gr { color: #E40000 } /* Generic.Error */
+.gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.gi { color: #008400 } /* Generic.Inserted */
+.go { color: #717171 } /* Generic.Output */
+.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
+.gs { font-weight: bold } /* Generic.Strong */
+.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.gt { color: #0044DD } /* Generic.Traceback */
+.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
+.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
+.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
+.kp { color: #008000 } /* Keyword.Pseudo */
+.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
+.kt { color: #B00040 } /* Keyword.Type */
+.m { color: #666666 } /* Literal.Number */
+.s { color: #BA2121 } /* Literal.String */
+.na { color: #687822 } /* Name.Attribute */
+.nb { color: #008000 } /* Name.Builtin */
+.nc { color: #0000FF; font-weight: bold } /* Name.Class */
+.no { color: #880000 } /* Name.Constant */
+.nd { color: #AA22FF } /* Name.Decorator */
+.ni { color: #717171; font-weight: bold } /* Name.Entity */
+.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
+.nf { color: #0000FF } /* Name.Function */
+.nl { color: #767600 } /* Name.Label */
+.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
+.nt { color: #008000; font-weight: bold } /* Name.Tag */
+.nv { color: #19177C } /* Name.Variable */
+.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
+.w { color: #bbbbbb } /* Text.Whitespace */
+.mb { color: #666666 } /* Literal.Number.Bin */
+.mf { color: #666666 } /* Literal.Number.Float */
+.mh { color: #666666 } /* Literal.Number.Hex */
+.mi { color: #666666 } /* Literal.Number.Integer */
+.mo { color: #666666 } /* Literal.Number.Oct */
+.sa { color: #BA2121 } /* Literal.String.Affix */
+.sb { color: #BA2121 } /* Literal.String.Backtick */
+.sc { color: #BA2121 } /* Literal.String.Char */
+.dl { color: #BA2121 } /* Literal.String.Delimiter */
+.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
+.s2 { color: #BA2121 } /* Literal.String.Double */
+.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
+.sh { color: #BA2121 } /* Literal.String.Heredoc */
+.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
+.sx { color: #008000 } /* Literal.String.Other */
+.sr { color: #A45A77 } /* Literal.String.Regex */
+.s1 { color: #BA2121 } /* Literal.String.Single */
+.ss { color: #19177C } /* Literal.String.Symbol */
+.bp { color: #008000 } /* Name.Builtin.Pseudo */
+.fm { color: #0000FF } /* Name.Function.Magic */
+.vc { color: #19177C } /* Name.Variable.Class */
+.vg { color: #19177C } /* Name.Variable.Global */
+.vi { color: #19177C } /* Name.Variable.Instance */
+.vm { color: #19177C } /* Name.Variable.Magic */
+.il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/doc/base/style.css b/doc/base/style.css
new file mode 100644
index 0000000..66349cb
--- /dev/null
+++ b/doc/base/style.css
@@ -0,0 +1,173 @@
+@import "/pygments-light.css" (prefers-color-scheme: light);
+@import "/pygments-dark.css" (prefers-color-scheme: dark);
+:root {
+    --overlay-bg: #8881;
+    --overlay-border: #8884;
+}
+
+/* Default font and layout */
+html {
+    box-sizing: border-box;
+    font-size: clamp(100%, 2vw, 150%);
+    margin: auto;
+    max-width: 36rem;
+}
+
+body { margin: 0 1rem }
+
+/* Text geometry */
+p, details {
+    hyphens: auto;
+    text-align: justify;
+}
+
+/* Headings */
+h2 a, h3 a, h4 a, h5 a, h6 a { color: CanvasText }
+
+.toc ol ol {
+    list-style-type: lower-alpha;
+}
+
+/* General formatting */
+li p { margin: 0 }
+a { text-decoration: none }
+p a:hover { text-decoration: underline }
+
+sup.footnote-ref > a::before { content: '[' }
+sup.footnote-ref > a::after { content: ']' }
+section.footnotes {
+    font-size: 80%;
+    border: none;
+    margin: 1ex 0;
+}
+section.footnotes > ol {
+    counter-reset: list;
+    list-style-position: inside;
+    padding-left: 0;
+}
+section.footnotes > ol > li { counter-increment: list }
+section.footnotes > ol > li::marker {
+    content: "[" counter(list, digit) "]\a0";
+}
+
+/* Images */
+figure {
+    margin: 0;
+    text-align: center;
+}
+img {
+    display: block;
+    margin: auto;
+    max-width: 100%;
+}
+
+/* Boxes */
+blockquote, .note {
+    margin: 1ex -1rem;
+    padding-bottom: 1ex;
+    padding-left: 0.75rem;
+    padding-right: 1rem;
+    padding-top: 1ex;
+}
+blockquote p, .note p { margin: 1ex 0 }
+.note p:first-child { font-weight: bold }
+blockquote {
+    background: var(--overlay-bg);
+    border-left: 0.25rem solid var(--overlay-border);
+}
+.note {
+    background-color: #2482;
+    border-left: 0.25rem solid #2484;
+}
+
+/* Header */
+header {
+    margin: 1.5rem -0.5rem;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    align-items: center;
+}
+
+header a {
+    border-bottom: solid;
+    font-weight: bold;
+    margin: 0 0.5rem;
+    text-decoration: none;
+}
+
+nav, nav li { display: inline-block }
+nav ul { margin: 0 }
+
+footer, .tags, .right {
+    color: GrayText;
+    font-size: 80%;
+}
+
+footer {
+    margin-top: 1.5rem;
+    margin-bottom: 2rem;
+}
+
+footer a, .tags a {
+    color: GrayText;
+    text-decoration: underline;
+}
+
+.nowrap { display: inline-block }
+.tags { float: left }
+.right {
+    float: right;
+    margin-left: auto;
+}
+
+/* Table */
+table {
+    border-bottom: 0.15em solid;
+    border-collapse: collapse;
+    border-top: 0.15em solid;
+    line-height: 1em;
+    margin-bottom: 1.5em;
+    margin-left: auto;
+    margin-right: auto;
+    text-align: center;
+}
+
+tr:first-of-type > th { border-bottom: 0.08em solid }
+tr, th, td { padding: 0.5em }
+
+/* highlight.js */
+code, .highlight { background-color: var(--overlay-bg) }
+.highlight {
+    display: block;
+    padding: 0 1ch;
+    margin: 0 -1ch;
+    overflow-x: auto;
+}
+.highlight > pre { margin: 1ex 0 }
+pre p { margin: 0 }
+
+.comment {
+    background-color: var(--overlay-bg);
+    clear: both;
+    margin: 1ex 0;
+    overflow: hidden;
+    padding: 0 1rem;
+}
+
+.fead {
+    display: flex;
+    flex-wrap: wrap;
+    margin: -0.5rem;
+    margin-bottom: 0;
+}
+.fead h3 { margin: 0 0 1ex }
+.fead article {
+    background: var(--overlay-bg);
+    display: flex;
+    flex-direction: column;
+    flex: 1 1 0;
+    margin: 1ex;
+    min-width: 12rem;
+    padding: 1ex;
+}
diff --git a/doc/html.xslt b/doc/html.xslt
new file mode 100644
index 0000000..5ad48ad
--- /dev/null
+++ b/doc/html.xslt
@@ -0,0 +1,63 @@
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:rub="https://rub.parody" extension-element-prefixes="rub">
+  <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
+  <xsl:template match="*"><rub:eval/></xsl:template>
+
+  <xsl:template match="/rub:page">
+    <html lang="en">
+      <head>
+        <meta charset="utf-8"/>
+        <meta name="color-scheme" content="dark light"/>
+        <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
+        <xsl:if test="rub:description != ''">
+          <meta name="description" content="{rub:description}"/>
+        </xsl:if>
+        <link rel="alternate" type="application/atom+xml" href="/index.xml"/>
+        <link rel="icon" href="/karderio-Toy-baloon.svg"/>
+        <link rel="stylesheet" href="/style.css"/>
+        <title><xsl:value-of select="rub:title"/></title>
+      </head>
+      <body>
+        <header>
+          <div class="blog-name"><a href="/">Rub</a></div>
+          <nav>
+            <ul>
+              <li><a href="/demo">Demoes</a></li>
+            </ul>
+          </nav>
+        </header>
+        <main>
+          <h1><xsl:value-of select="rub:title"/></h1>
+          <xsl:apply-templates select="rub:markdown"/>
+        </main>
+        <xsl:if test="boolean(rub:category)">
+          <small class="tags">
+            <strong>Tags:</strong>
+            <xsl:for-each select="rub:category">
+              <xsl:text> </xsl:text>
+              <a href="/tag/{.}"><xsl:value-of select="."/></a>
+            </xsl:for-each>
+          </small>
+          <br/>
+        </xsl:if>
+        <footer>
+          Copyright and stuff
+        </footer>
+      </body>
+    </html>
+  </xsl:template>
+
+  <xsl:template match="h2|h3|h4|h5|h6">
+    <xsl:element name="{name()}">
+      <xsl:attribute name="id">
+        <xsl:value-of select="text()"/>
+      </xsl:attribute>
+      <a href="#{text()}"><xsl:value-of select="."/></a>
+    </xsl:element>
+  </xsl:template>
+
+  <!-- Remove paragraph inside footnote list item -->
+  <xsl:template match="li[starts-with(@id, 'fn-')]/p">
+    <xsl:copy-of select='@*|node()'/>
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/doc/pages/demo/katex/index.xml b/doc/pages/demo/katex/index.xml
new file mode 100644
index 0000000..e7ecb02
--- /dev/null
+++ b/doc/pages/demo/katex/index.xml
@@ -0,0 +1,115 @@
+<page xmlns="https://rub.parody">
+<title>KaTeX demo</title>
+<description>Math formulae to demo LaTeX to MathML rendering</description>
+<date>2020-04-15</date>
+<category>demo</category>
+<category>math</category>
+<markdown>
+Given two discrete-time systems <m>A</m> and <m>B</m> connected in cascade
+to form a new system <m>C = x \mapsto B(A(x))</m>, we examine
+the following properties:
+
+## Linearity
+
+If <m>A</m> and <m>B</m> are linear,
+i.e. for all signals <m>x_i</m> and scalars <m>a_i</m>,
+
+<math>
+\begin{aligned}
+  A\left(n \mapsto \sum_i a_i x_i[n]\right) = n \mapsto \sum_i a_i A(x_i)[n]\\
+  B\left(n \mapsto \sum_i a_i x_i[n]\right) = n \mapsto \sum_i a_i B(x_i)[n]
+\end{aligned}
+</math>
+
+then <m>C</m> is also linear
+
+<math><![CDATA[
+\begin{aligned}
+  C\left(n \mapsto \sum_i a_i x_i[n]\right)
+  &= B\left(A\left(n \mapsto \sum_i a_i x_i[n]\right)\right)\\
+  &= B\left(n \mapsto \sum_i a_i A(x_i)[n]\right)\\
+  &= n \mapsto \sum_i a_i B(A(x_i))[n]\\
+  &= n \mapsto \sum_i a_i C(x_i)[n]
+\end{aligned}
+]]></math>
+
+## Time Invariance
+
+If <m>A</m> and <m>B</m> are time invariant,
+i.e. for all signals <m>x</m> and integers <m>k</m>,
+
+<math><![CDATA[
+\begin{aligned}
+  A(n \mapsto x[n - k]) &= n \mapsto A(x)[n - k]\\
+  B(n \mapsto x[n - k]) &= n \mapsto B(x)[n - k]
+\end{aligned}
+]]></math>
+
+then <m>C</m> is also time invariant
+
+<math><![CDATA[
+\begin{aligned}
+  C(n \mapsto x[n - k])
+  &= B(A(n \mapsto x[n - k]))\\
+  &= B(n \mapsto A(x)[n - k])\\
+  &= n \mapsto B(A(x))[n - k]\\
+  &= n \mapsto C(x)[n - k]
+\end{aligned}
+]]></math>
+
+## LTI Ordering
+
+If <m>A</m> and <m>B</m> are linear and time-invariant, there exists
+signals <m>g</m> and <m>h</m> such that for all signals <m>x</m>,
+<m>A = x \mapsto x * g</m> and <m>B = x \mapsto x * h</m>, thus 
+
+<math>
+B(A(x)) = B(x * g) = x * g * h = x * h * g = A(x * h) = A(B(x))
+</math>
+
+or interchanging <m>A</m> and <m>B</m> order does not change <m>C</m>.
+
+## Causality
+
+If <m>A</m> and <m>B</m> are causal,
+i.e. for all signals <m>x</m>, <m>y</m> and any choise of integer <m>k</m>,
+
+<math><![CDATA[
+\begin{aligned}
+  \forall n < k, x[n] = y[n]\quad
+  \Longrightarrow &\;\begin{cases}
+  \forall n < k, A(x)[n] = A(y)[n]\\
+  \forall n < k, B(x)[n] = B(y)[n]
+  \end{cases}\\
+  \Longrightarrow &\;\forall n < k, B(A(x))[n] = B(A(y))[n]\\
+  \Longleftrightarrow &\;\forall n < k, C(x)[n] = C(y)[n]
+\end{aligned}
+]]></math>
+
+then <m>C</m> is also causal.
+
+## BIBO Stability
+
+If <m>A</m> and <m>B</m> are stable, i.e. there exists a signal <m>x</m>
+and scalars <m>a</m> and <m>b</m> that for all integers <m>n</m>,
+
+<math><![CDATA[
+\begin{aligned}
+  |x[n]| < a &\Longrightarrow |A(x)[n]| < b\\
+  |x[n]| < a &\Longrightarrow |B(x)[n]| < b
+\end{aligned}
+]]></math>
+
+then <m>C</m> is also stable, i.e. there exists a signal <m>x</m>
+and scalars <m>a</m>, <m>b</m> and <m>c</m> that for all integers <m>n</m>,
+
+<math><![CDATA[
+\begin{aligned}
+  |x[n]| < a\quad
+  \Longrightarrow &\;|A(x)[n]| < b\\
+  \Longrightarrow &\;|B(A(x))[n]| < c\\
+  \Longleftrightarrow &\;|C(x)[n]| < c
+\end{aligned}
+]]></math>
+</markdown>
+</page>
diff --git a/doc/pages/demo/pygments/index.xml b/doc/pages/demo/pygments/index.xml
new file mode 100644
index 0000000..361c6ab
--- /dev/null
+++ b/doc/pages/demo/pygments/index.xml
@@ -0,0 +1,66 @@
+<page xmlns="https://rub.parody">
+<title>Pygments demo</title>
+<description>Code snippets to demonstrate syntax highlighting</description>
+<date>2022-12-26</date>
+<category>demo</category>
+<category>aoc</category>
+<markdown>
+Syntax highlighting for D using Pygments:
+
+<highlight lang="d"><![CDATA[
+import core.stdc.stdio : getchar, printf;
+
+extern(C) void main()
+{
+    slide: for (auto q = 0u, i = 1u; q & 0xffu ^ '\n'; ++i)
+    {
+        q <<= 8u;
+        q |= getchar();
+        if (i < 4)
+            continue;
+
+        auto p = cast(ubyte*) &q;
+        for (auto s = 0u, j = 0u; j < 4u; ++j)
+        {
+            auto b = 1u << (p[j] & 0x1fu);
+            if (s & b)
+                continue slide;
+            s |= b;
+        }
+
+        printf("%d\n", i);
+        break;
+    }
+}
+]]></highlight>
+
+The same but let Pygments guess the language:
+
+<highlight><![CDATA[
+import core.stdc.stdio : getchar, printf;
+
+extern(C) void main()
+{
+    slide: for (auto q = 0u, i = 1u; q & 0xffu ^ '\n'; ++i)
+    {
+        q <<= 8u;
+        q |= getchar();
+        if (i < 4)
+            continue;
+
+        auto p = cast(ubyte*) &q;
+        for (auto s = 0u, j = 0u; j < 4u; ++j)
+        {
+            auto b = 1u << (p[j] & 0x1fu);
+            if (s & b)
+                continue slide;
+            s |= b;
+        }
+
+        printf("%d\n", i);
+        break;
+    }
+}
+]]></highlight>
+</markdown>
+</page>
diff --git a/doc/pages/index.xml b/doc/pages/index.xml
new file mode 100644
index 0000000..1a77965
--- /dev/null
+++ b/doc/pages/index.xml
@@ -0,0 +1,14 @@
+<page xmlns="https://rub.parody">
+<title>Rub</title>
+<markdown>
+Rub is a static generator framework.
+
+## Demoes
+
+* [MathML rendering](/demo/katex) via [KaTeX]
+* [Syntax highlighting](/demo/pygments) via [Pygments]
+
+[KaTeX]: https://katex.org
+[Pygments]: https://pygments.org
+</markdown>
+</page>
diff --git a/doc/rss.xslt b/doc/rss.xslt
new file mode 100644
index 0000000..48f9203
--- /dev/null
+++ b/doc/rss.xslt
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:rub="https://rub.parody" extension-element-prefixes="rub"
+                xmlns:content="http://purl.org/rss/1.0/modules/content/">
+  <xsl:template match="/feed">
+    <rss version="2.0">
+      <channel>
+        <title>Demo RSS feed</title>
+        <link>https://rub.parody/</link>
+        <description>Global feed</description>
+        <generator>Rub</generator>
+        <xsl:for-each select="entry">
+          <item>
+            <title><xsl:value-of select="title"/></title>
+            <description><xsl:value-of select="description"/></description>
+            <content:encoded>
+              <xsl:apply-templates select="html/body/main"/>
+            </content:encoded>
+          </item>
+        </xsl:for-each>
+      </channel>
+    </rss>
+  </xsl:template>
+
+  <xsl:template match="main"><rub:serialize/></xsl:template>
+</xsl:stylesheet>
diff --git a/doc/rub b/doc/rub
new file mode 100755
index 0000000..d1113a2
--- /dev/null
+++ b/doc/rub
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+from contextlib import contextmanager
+from functools import partial
+from pathlib import Path
+from shutil import which
+from subprocess import PIPE, Popen
+from urllib.request import Request, urlopen
+from xml.sax.saxutils import unescape
+
+from lxml.etree import Element, XML
+from lxml.html import (fragment_fromstring as from_html_fragment,
+                       fragments_fromstring as from_html_fragments)
+from mistune import html as from_md
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name, guess_lexer
+from rub import rub, xml
+
+PYGMENTS_FORMATTER = HtmlFormatter(lineseparator='<br/>')
+KATEX_SERVER = '''
+var http = require('http');
+var katex = require('katex');
+
+let server = http.createServer((request, response) => {
+  if (request.method === 'POST') {
+    response.writeHead(200, 'OK');
+    var body = '';
+    request.on('data', (chunk) => body += chunk);
+    request.on('end', () => {
+      response.write(katex.renderToString(body, {
+        displayMode: request.headers['x-katex-mode'] === 'display',
+        output: 'mathml',
+      }));
+      response.end();
+    });
+  } else {
+    response.writeHead(405, 'Method Not Allowed');
+    response.end();
+  }
+});
+server.listen(() => {
+  console.log('http://localhost:' + server.address().port);
+});
+'''
+
+
+def mistune(extension, context, input_node, output_parent):
+    """Render Markdown to HTML."""
+    tmp = Element('tmp')
+    xml.recurse(extension, context, input_node, tmp)
+    tmp[0].text = input_node.text
+    text = xml.serialize_content(tmp[0]).decode()
+    for i in from_html_fragments(from_md(unescape(text))):
+        extension.apply_templates(context, i, output_parent)
+
+
+def pygments(extension, context, input_node, output_parent):
+    """Highlight code syntax in HTML."""
+    code = input_node.text
+    lang = input_node.get('lang')
+    lexer = guess_lexer(code) if lang is None else get_lexer_by_name(lang)
+    output = from_html_fragment(highlight(code, lexer, PYGMENTS_FORMATTER))
+    output.tail = input_node.tail
+    output_parent.append(output)
+
+
+def katex(extension, context, input_node, output_parent, url, display=False):
+    """Render LaTeX to content MathML."""
+    with urlopen(Request(url, xml.serialize_content(input_node),
+                         {'X-KaTeX-Mode': 'display'} if display else {})) as r:
+        output = XML(r.read())[0]  # remove span[@class='katex']
+        output.tail = input_node.tail
+        output_parent.append(output)
+
+
+@contextmanager
+def katex_server():
+    server = Popen(which('node'), stdin=PIPE, stdout=PIPE, text=True)
+    try:
+        server.stdin.write(KATEX_SERVER)
+        server.stdin.close()
+        yield partial(katex, url=server.stdout.readline().strip())
+    finally:
+        server.kill()
+
+
+with katex_server() as mathml:
+    wd = Path(__file__).parent
+    rub(xml.Processor(wd/'html.xslt',
+                      partial(Path.with_suffix, suffix='.html'),
+                      markdown=mistune, highlight=pygments,
+                      math=partial(mathml, display=True), m=mathml),
+        xml.Processor(wd/'rss.xslt',
+                      partial(Path.with_name, name='index.xml')),
+        wd/'base', wd/'pages', wd/'cache', wd/'out')
diff --git a/doc/shell.nix b/doc/shell.nix
new file mode 100644
index 0000000..453398c
--- /dev/null
+++ b/doc/shell.nix
@@ -0,0 +1,10 @@
+with import <nixpkgs> {};
+mkShell {
+  packages = (with python3Packages; [
+    doit lxml mistune pygments
+  ]) ++ [ nodejs nodePackages.katex ];
+
+  shellHook = ''
+    PYTHONPATH=../src:$PYTHONPATH
+  '';
+}