Compare commits

..

10 Commits

Author SHA1 Message Date
sxlxc 55719f3444 fix a tui bug 2026-03-26 15:57:32 +08:00
sxlxc d4629ec8e7 update readme 2026-03-26 15:57:02 +08:00
sxlxc e419366615 better tui for watch 2026-03-24 21:05:23 +08:00
sxlxc 6c59abb9cc Introduce CLI, watch server, and bundled assets
Add data-files and bundled templates/fonts/css to the package and
rename the executable to hakysidian (autogen Paths_hakysidian).
Refactor site.hs to parse CLI commands (build/clean/rebuild/watch),
start a preview server, snapshot inputs and run an incremental watch
loop, and move rules into a siteRules function. Update ChaoDoc and
filters to accept math-macros, and add favicon links to templates.
2026-03-24 20:47:46 +08:00
sxlxc 6a3b4c5f88 Remove hakyll-blog.cabal and update .Proof CSS 2026-03-24 20:06:48 +08:00
sxlxc 720a19e24d Remove .notes-list custom styles 2026-03-24 20:01:35 +08:00
sxlxc 1789c75f18 Revert "Rename package to hakysidian; add CLI and assets"
This reverts commit 3d2c5a8852.
2026-03-24 19:58:26 +08:00
sxlxc 3d2c5a8852 Rename package to hakysidian; add CLI and assets 2026-03-24 18:59:06 +08:00
sxlxc 614de591ba Add notes list and integrate into templates
New partial templates/notes-list.html to render the notes list.
Add .notes-list CSS for styling and ensure contents areas use it.
Refactor site.hs: add loadNoteLinks and provide a "notes" listField
used by index and note pages.
2026-03-24 14:56:45 +08:00
sxlxc 71611b0641 Increase body line-height and add theorem padding 2026-03-23 14:54:09 +08:00
14 changed files with 1111 additions and 903 deletions
+41 -81
View File
@@ -1,130 +1,90 @@
.theorem-environment {
position: relative;
margin: 1.5rem 0;
padding: 1rem 1.25rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
font-style: italic;
}
.theorem-environment > :first-child {
margin-top: 0;
}
.theorem-environment > :last-child {
margin-bottom: 0;
font-style: italic;
margin-top: 1em;
padding: 0.5em;
background-color: whitesmoke;
}
.theorem-header {
color: var(--color-text);
font-weight: 700;
font-style: normal;
font-weight: bold;
font-style: normal;
}
.theorem-header .index:before {
content: " ";
content: ' ';
}
.theorem-header .name:before {
content: " (";
content: ' (';
}
.theorem-header .name:after {
content: ")";
content: ')';
}
.theorem-header:after {
content: ".\2002\2002";
content: '.\2002\2002';
}
.theorem-header + p {
display: inline;
}
.Proof {
padding-right: 2.5rem;
font-style: normal;
.theorem-header+p {
display: inline;
}
.Proof .type {
color: var(--color-linkh);
font-weight: 400;
font-style: italic;
font-style: italic;
font-weight: normal;
}
.Proof {
background: none;
font-style: normal;
position: relative;
}
.Proof:after {
content: "\220e";
position: absolute;
right: 1rem;
bottom: 0.85rem;
color: var(--color-tag1);
content: '∎';
position: absolute;
right: 0px;
bottom: 0px;
}
table.postindex {
width: 100%;
border: 0;
border-collapse: separate;
border-spacing: 0;
background: transparent;
box-shadow: none;
width: 100%;
}
table.postindex cite {
font-style: normal;
}
table.postindex td {
padding: 0.65rem 0;
border: 0;
border-bottom: 1px solid var(--color-border);
background: transparent;
}
table.postindex tr:nth-child(even) td {
background: transparent;
}
table.postindex tr:last-child td {
border-bottom: 0;
font-style: normal;
}
table.postindex td.right {
width: 11ex;
color: var(--color-tag1);
text-align: right;
white-space: nowrap;
text-align: right;
width: 11ex;
}
.header-section-number {
margin-right: 0.55rem;
color: var(--color-tag1);
margin-right: 10px;
}
.header-section-number:after {
content: ".";
content: '.';
}
.csl-entry {
display: table;
width: 100%;
margin-bottom: 0.75rem;
table-layout: auto;
display: table;
width: 100%;
table-layout: auto;
}
.csl-left-margin {
display: table-cell;
width: 1px;
padding-right: 0.75em;
color: var(--color-tag1);
white-space: nowrap;
display: table-cell;
padding-right: 0.5em;
white-space: nowrap;
width: 1px;
}
.csl-right-inline {
display: table-cell;
}
.csl-right-inline a {
word-break: break-all;
display: table-cell;
}
.csl-right-inline a{
word-break: break-all;
}
+147 -448
View File
@@ -1,170 +1,69 @@
:root {
color-scheme: light;
--color-text: #162031;
--color-tag1: #6c788c;
--color-tag2: #547157;
--color-bg: #f3f6fb;
--color-surface: rgba(255, 255, 255, 0.84);
--color-surface-strong: #ffffff;
--color-surface-muted: #edf3fb;
--color-border: rgba(22, 32, 49, 0.12);
--color-border-strong: rgba(22, 32, 49, 0.2);
--color-link: #225b9c;
--color-link-line: rgba(34, 91, 156, 0.28);
--color-linkhbg: rgba(89, 143, 211, 0.16);
--color-linkh: #163a63;
--color-bq: #628b72;
--color-notice: #d0455a;
--color-shadow: 0 20px 50px rgba(15, 23, 42, 0.08);
--color-shadow-soft: 0 10px 30px rgba(15, 23, 42, 0.06);
--code-bg: #f6f8fc;
--code-border: rgba(55, 65, 81, 0.14);
--page-glow-1: rgba(144, 187, 241, 0.18);
--page-glow-2: rgba(99, 141, 118, 0.14);
--color-text: black;
--color-tag1: gray;
--color-tag2: darkolivegreen;
--color-bg: white;
--color-link: #337ab7;
--color-linkhbg: #e6f0ff;
--color-linkh: #002266;
--color-bq: olivedrab;
--color-notice: #fb4f4f;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--color-text: #e7edf7;
--color-tag1: #9aa7bb;
--color-tag2: #9abf9c;
--color-bg: #0b1020;
--color-surface: rgba(15, 23, 42, 0.78);
--color-surface-strong: #101827;
--color-surface-muted: #162238;
--color-border: rgba(148, 163, 184, 0.18);
--color-border-strong: rgba(148, 163, 184, 0.26);
--color-link: #8bc3ff;
--color-link-line: rgba(139, 195, 255, 0.35);
--color-linkhbg: rgba(139, 195, 255, 0.16);
--color-linkh: #d9ecff;
--color-bq: #8db59a;
--color-notice: #ff8b8b;
--color-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
--color-shadow-soft: 0 12px 32px rgba(0, 0, 0, 0.3);
--code-bg: #0f172a;
--code-border: rgba(148, 163, 184, 0.22);
--page-glow-1: rgba(71, 119, 183, 0.22);
--page-glow-2: rgba(74, 118, 90, 0.18);
--color-text: white;
--color-bg: black;
}
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scrollbar-gutter: stable;
scroll-behavior: smooth;
font-size: 14pt;
}
body {
box-sizing: content-box;
margin: 0;
font-family: "Lato", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-size: 1rem;
line-height: 1.5;
line-height: 140%;
color: var(--color-text);
background:
radial-gradient(circle at top, var(--page-glow-1), transparent 38%),
radial-gradient(circle at right top, var(--page-glow-2), transparent 30%),
var(--color-bg);
background-attachment: fixed;
background-color: var(--color-bg);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
body.lang-zh {
text-align: left;
}
::selection {
background-color: var(--color-linkhbg);
color: var(--color-linkh);
}
main,
.text-space {
min-width: 0;
}
body a {
color: var(--color-link);
text-decoration: underline;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.18em;
text-decoration-color: var(--color-link-line);
transition:
color 0.18s ease,
background-color 0.18s ease,
text-decoration-color 0.18s ease;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
body a:visited {
color: var(--color-link);
text-decoration: none;
}
.text-space a:hover {
background-color: var(--color-linkhbg);
color: var(--color-linkh);
text-decoration-color: transparent;
border-radius: 0.2rem;
text-decoration: none;
}
body a:focus-visible {
outline: 2px solid var(--color-link);
outline-offset: 3px;
border-radius: 0.25rem;
}
details {
margin: 1.25rem 0;
padding: 0.85rem 1rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
background-color: var(--color-linkhbg);
}
details[open] summary {
margin-bottom: 0.75rem;
}
details > :last-child {
margin-bottom: 0;
}
summary {
font-weight: 700;
}
summary:hover {
cursor: pointer;
color: var(--color-linkh);
}
/* mathML */
.htmlmathparagraph,
mtext,
math {
font-family: "Lete Sans Math";
/*mathML*/
.htmlmathparagraph, mtext,math {
font-family: Lete Sans Math;
}
.math-container,
#math-container {
display: block;
overflow-x: auto;
overflow-y: hidden;
padding: 0.5em;
padding: .5em;
}
.math-container.math-container-tagged {
@@ -173,7 +72,7 @@ math {
align-items: center;
column-gap: 1rem;
overflow: visible;
padding: 0.5em 0;
padding: .5em 0;
}
.math-container.math-container-tagged .math-tag-spacer {
@@ -184,7 +83,7 @@ math {
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding: 0.5em 0;
padding: .5em 0;
}
.math-container.math-container-tagged .math-tag {
@@ -206,7 +105,6 @@ math {
}
p {
margin: 0 0 1rem;
hyphens: auto;
}
@@ -219,25 +117,21 @@ header {
font-family: "IosevkaC", sans-serif;
}
/* top bar*/
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.navright {
display: flex;
flex-wrap: wrap;
}
.navright a {
margin: 0 0 0 1em;
}
/* Links inside the navbar */
.navbar a {
color: var(--color-text);
text-decoration: none;
color: var(--color-text);
}
.navbar a:visited {
@@ -245,32 +139,37 @@ header {
}
nav {
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
text-align: right;
border-bottom: 1px solid var(--color-border-strong);
border-bottom: solid 1px var(--color-text);
}
nav a {
display: inline-flex;
align-items: center;
font-size: 1.2rem;
/*margin-left: 0.5em;*/
display: inline-block;
vertical-align: middle;
font-size: 1.05rem;
text-decoration: none;
}
.uri {
overflow-wrap: anywhere;
word-break: break-word;
word-wrap: break-word;
/* Legacy support */
overflow-wrap: break-word;
/* Modern property */
word-break: break-all;
/* Break long words if necessary */
white-space: normal;
/* Allow wrapping */
}
footer {
margin-top: 3rem;
padding-right: 0.2rem;
color: var(--color-tag1);
font-size: 0.82rem;
color: var(--color-text);
font-size: 0.8rem;
margin-top: 2em;
text-align: right;
padding-right: 1em;
}
h1,
@@ -280,63 +179,47 @@ h4,
h5,
h6 {
text-align: left;
text-wrap: balance;
}
.pagetitle {
margin: 0;
font-size: 2rem;
font-weight: 400;
font-weight: normal;
font-style: normal;
text-align: left;
line-height: 1;
letter-spacing: -0.03em;
line-height: 100%;
}
h1 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
margin-top: 1em;
font-size: 1.44rem;
font-weight: 700;
font-weight: bold;
font-style: normal;
line-height: 1.1;
letter-spacing: -0.02em;
text-align: left;
line-height: 100%;
}
h2 {
margin-top: 1.75rem;
margin-bottom: 0.7rem;
margin-top: 1em;
font-size: 1.2rem;
font-weight: 700;
font-style: normal;
line-height: 1.2;
font-weight: bold;
font-style: normal
}
h3 {
margin-top: 1.5rem;
margin-bottom: 0.6rem;
margin-top: 1em;
font-size: 1rem;
font-weight: 700;
font-style: normal;
line-height: 1.25;
}
h4,
h5,
h6 {
margin-top: 1.35rem;
margin-bottom: 0.55rem;
line-height: 1.3;
font-weight: bold;
font-style: normal
}
article .header {
color: var(--color-tag1);
font-size: 1rem;
font-style: normal;
color: var(--color-tag1);
text-align: left;
letter-spacing: 0.02em;
}
.info {
color: var(--color-tag2);
font-size: 1rem;
@@ -356,375 +239,202 @@ article .header {
}
section.body {
margin-top: 1.75rem;
}
section.body > :first-child {
margin-top: 0;
margin-top: 2rem;
}
.ascii-art {
overflow-x: auto;
font-family: monospace;
line-height: 1.2;
line-height: normal;
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.25rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-left: 4px solid var(--color-bq);
border-radius: 0 1rem 1rem 0;
box-shadow: var(--color-shadow-soft);
margin: 1rem 0;
padding: 0 0 0 1.5em;
border-left: 3px solid var(--color-bq);
}
blockquote p {
margin: 0;
}
ol,
ul {
margin: 0 0 1.1rem;
ol {
padding-left: 2em;
}
ul {
list-style-type: square;
padding-left: 2em;
}
li {
margin-bottom: 0.3em;
padding-left: 0.15em;
}
li::marker {
color: var(--color-tag2);
}
table {
margin: 1.5rem 0;
border: 1px solid var(--color-border-strong);
border-collapse: collapse;
background-color: var(--color-surface-strong);
box-shadow: var(--color-shadow-soft);
margin-bottom: 0.15em;
}
table,
th,
td {
padding: 0.65rem 0.8rem;
border: 1px solid var(--color-border);
border: 1px solid darkolivegreen;
border-collapse: collapse;
text-align: left;
vertical-align: top;
}
th {
background-color: var(--color-surface-muted);
}
tr:nth-child(even) td {
background-color: var(--color-surface);
}
figure {
display: flex;
flex-flow: column;
gap: 0.75rem;
padding: 0.9rem;
margin: 1.5rem auto;
padding: 5px;
margin: auto;
max-width: 80%;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1.25rem;
box-shadow: var(--color-shadow-soft);
}
figure img {
border-radius: 0.9rem;
}
figcaption {
padding: 0 0.25rem;
color: var(--color-tag1);
font-size: 0.92rem;
/* font: italic smaller sans-serif; */
padding: 3px;
text-align: center;
}
.caption {
display: none;
display: none
}
.centerimg img {
margin: 0 auto 0 auto;
display: block;
margin: 0 auto;
}
pre,
div.highlight {
margin: 1.5rem 0;
padding: 1rem 1.1rem;
div.highlight,
pre code {
margin: auto;
padding: 10px;
overflow: auto;
background-color: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
}
pre code,
div.highlight pre,
div.highlight pre code {
display: block;
margin: 0;
padding: 0;
overflow: visible;
background: transparent;
border: 0;
box-shadow: none;
}
code {
display: inline;
margin: 0;
padding: 0.12em 0.35em;
background-color: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 0.45rem;
font-family: "IosevkaC", monospace;
font-size: 0.92em;
margin: 0 auto;
display: inline-block;
padding: 0px 2px;
border-radius: 2px;
font-variant-ligatures: none;
font-kerning: none;
text-rendering: optimizeSpeed;
}
.draft-notice {
display: table;
margin: 1.5rem auto;
padding: 0.85rem 1rem;
color: var(--color-notice);
text-align: center;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-left: 4px solid var(--color-notice);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
margin: 1em auto;
text-align: center
}
.subtitle {
margin-top: 0.5rem;
color: var(--color-tag1);
font-size: 1.2rem;
line-height: 1.4;
text-align: left;
font-size: 1.2rem;
margin-top: 0
}
.subtitle p {
margin: 0.2rem 0;
}
.gallery {
margin-top: 2em;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 2em;
gap: 12px;
}
.gallery img {
display: block;
width: 100%;
max-width: 320px;
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
display: block;
}
hr {
margin: 2rem 0;
border: 0;
border-top: 1px solid var(--color-border-strong);
/* phones -- no sidebar no sidenotes*/
@media (max-width: 768px) {
body {
/* width: 90%; */
margin: auto;
padding: 0 5%;
text-align: left;
max-width: 876px;
}
mjx-container[display="true"]
/*, .katex-display */ {
overflow-x: auto;
overflow-y: hidden;
}
/* .katex-display>.katex>.katex-html>.tag {
display: inline-block;
position: relative;
padding-left: 10pt;
} */
}
.toc {
display: none;
}
/* phones: no sidebar, no sidenotes */
@media (max-width: 768px) {
body {
max-width: 876px;
margin: auto;
padding: 1rem 5% 3rem;
text-align: left;
background-attachment: scroll;
}
mjx-container[display="true"] {
overflow-x: auto;
overflow-y: hidden;
}
figure {
max-width: 100%;
}
}
/* sidebar, no sidenotes */
/* sidebar. no sidenotes */
@media (min-width: 769px) {
body {
display: flex;
max-width: 1350px;
display: -webkit-flex;
-webkit-flex-flow: row wrap;
display: -ms-flexbox;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
width: 95%;
max-width: 1350px;
padding-right: 5%;
margin: auto;
padding: 1.5rem 5% 3rem 0;
}
.toc {
display: inline-block;
width: 33%;
margin-top: 4.5rem;
margin-top: 5rem;
margin-left: 0;
margin-right: 0;
width: 33%;
display: inline-block;
}
div#contents ul,
div#contents-big ul {
margin-top: 0.3em;
margin-bottom: 0.3em;
margin-left: 0.1rem;
padding-left: 1.45rem;
line-height: 1.25;
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 1em;
line-height: 1.2;
list-style-type: decimal;
margin-left: 0
}
div#contents ul.notes-list,
div#contents-big ul.notes-list {
list-style: none;
padding-left: 0;
}
div#contents-big ul ul {
list-style-type: none;
margin-top: 0.15rem;
margin-left: 0.2rem;
padding-left: 0.8rem;
border-left: 1px solid var(--color-border);
}
div#contents-big li + li {
margin-top: 0.18rem;
}
div#contents-big li::marker {
color: var(--color-tag1);
font-family: "IosevkaC", sans-serif;
div#contents-big li+li {
margin-top: 0.5em
}
div#contents-big {
position: sticky;
top: 6rem;
max-height: calc(100vh - 7rem);
max-height: calc(100dvh - 7rem);
left: 100%;
clear: both;
display: flex;
flex-direction: column;
max-width: 68%;
margin-right: 4em;
padding: 0.8rem;
background-color: var(--color-surface);
background-clip: padding-box;
border: 1px solid var(--color-border);
border-radius: 1.25rem;
box-shadow: var(--color-shadow-soft);
backdrop-filter: blur(14px);
font-size: 80%;
padding-top: 0;
padding-left: 1rem;
text-align: left;
}
div#contents-big .toc-scroll {
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: var(--color-border-strong) transparent;
}
div#contents-big .toc-scroll::-webkit-scrollbar {
width: 0.7rem;
}
div#contents-big .toc-scroll::-webkit-scrollbar-track {
margin-block: 0.45rem;
background: transparent;
}
div#contents-big .toc-scroll::-webkit-scrollbar-thumb {
background-color: var(--color-border-strong);
border: 0.18rem solid transparent;
border-radius: 999px;
background-clip: content-box;
}
div#contents-big .toc-scroll::-webkit-scrollbar-thumb:hover {
background-color: var(--color-tag1);
max-width: 60%;
clear: both;
margin-right: 4em;
position: sticky;
top: 5rem;
left: 100%
}
div#contents-big .mini-header {
margin: 0 0 0.45rem;
color: var(--color-tag1);
font-weight: 700;
font-weight: bold;
margin: 0;
font-variant: small-caps;
letter-spacing: 0.08em;
}
div#contents-big a {
color: var(--color-text);
text-decoration: none;
}
div#contents-big li > a {
display: block;
margin: 0 0.2rem;
padding: 0.14rem 0.32rem;
border-radius: 0.55rem;
border: 1px solid transparent;
transition:
color 0.18s ease,
background-color 0.18s ease,
border-color 0.18s ease,
transform 0.18s ease;
}
div#contents-big a:visited {
color: var(--color-text);
}
div#contents-big a:hover {
background-color: var(--color-linkhbg);
color: var(--color-linkh);
}
div#contents-big li > a:hover,
div#contents-big li > a:focus-visible {
border-color: var(--color-border);
transform: translateX(2px);
}
div#contents-big li > a:focus-visible,
div#contents-big .mini-header a:focus-visible {
outline: 2px solid var(--color-link);
outline-offset: 2px;
}
div#contents-big .mini-header a {
display: inline;
margin-left: 0.25rem;
color: var(--color-link);
font-size: 0.85rem;
line-height: 1;
vertical-align: baseline;
transition: color 0.18s ease;
}
div#contents-big .mini-header a:hover {
background-color: transparent;
color: var(--color-linkh);
transform: none;
}
.text-space {
@@ -733,8 +443,7 @@ hr {
max-width: 875px;
}
}
/* sidebar + sidenotes */
/* sidebar+sidenotes */
@media (min-width: 1200px) {
body {
width: 75%;
@@ -743,6 +452,7 @@ hr {
}
@media print {
.no-print,
.no-print * {
display: none !important;
@@ -750,16 +460,5 @@ hr {
body {
margin: auto;
background: white;
}
blockquote,
details,
div#contents-big,
figure,
pre,
div.highlight,
table {
box-shadow: none;
}
}
+33 -77
View File
@@ -1,79 +1,35 @@
:root {
--syntax-alert: #c2410c;
--syntax-annotation: #0f766e;
--syntax-attribute: #2563eb;
--syntax-number: #7c3aed;
--syntax-control: #4d7c0f;
--syntax-char: #0f766e;
--syntax-constant: #9f1239;
--syntax-comment: #7b8794;
--syntax-doc: #b45309;
--syntax-type-bg: #efe7ff;
--syntax-error: #dc2626;
--syntax-function: #0f4c81;
--syntax-import: #0d9488;
--syntax-preprocessor: #92400e;
--syntax-special: #0369a1;
--syntax-string: #15803d;
--syntax-variable: #1d4ed8;
}
@media (prefers-color-scheme: dark) {
:root {
--syntax-alert: #fdba74;
--syntax-annotation: #99f6e4;
--syntax-attribute: #93c5fd;
--syntax-number: #d8b4fe;
--syntax-control: #bef264;
--syntax-char: #86efac;
--syntax-constant: #fda4af;
--syntax-comment: #94a3b8;
--syntax-doc: #fcd34d;
--syntax-type-bg: rgba(216, 180, 254, 0.16);
--syntax-error: #f87171;
--syntax-function: #bfdbfe;
--syntax-import: #5eead4;
--syntax-preprocessor: #fdba74;
--syntax-special: #7dd3fc;
--syntax-string: #86efac;
--syntax-variable: #c4b5fd;
}
code.sourceCode
{
background: inherit
}
code.sourceCode {
background: inherit;
}
pre > code.sourceCode > span > a:first-child::before {
text-decoration: underline;
}
code span.al { color: var(--syntax-alert); font-weight: 700; }
code span.an { color: var(--syntax-annotation); font-weight: 700; font-style: italic; }
code span.at { color: var(--syntax-attribute); }
code span.bn { color: var(--syntax-number); }
code span.bu { color: var(--syntax-function); }
code span.cf { color: var(--syntax-control); font-weight: 700; }
code span.ch { color: var(--syntax-char); }
code span.cn { color: var(--syntax-constant); }
code span.co { color: var(--syntax-comment); font-style: italic; }
code span.cv { color: var(--syntax-annotation); font-weight: 700; font-style: italic; }
code span.do { color: var(--syntax-doc); font-style: italic; }
code span.dt { background-color: var(--syntax-type-bg); }
code span.dv { color: var(--syntax-number); }
code span.er { color: var(--syntax-error); font-weight: 700; }
code span.ex { color: var(--syntax-function); }
code span.fl { color: var(--syntax-number); }
code span.fu { color: var(--syntax-function); }
code span.im { color: var(--syntax-import); }
code span.in { color: var(--syntax-annotation); font-weight: 700; font-style: italic; }
code span.kw { color: var(--syntax-control); font-weight: 700; }
code span.op { color: var(--syntax-function); font-weight: 700; }
code span.ot { color: var(--syntax-special); font-weight: 700; }
code span.pp { color: var(--syntax-preprocessor); }
code span.sc { color: var(--syntax-special); }
code span.ss { color: var(--syntax-number); }
code span.st { color: var(--syntax-string); }
code span.va { color: var(--syntax-variable); }
code span.vs { color: var(--syntax-special); }
code span.wa { color: var(--syntax-annotation); font-weight: 700; font-style: italic; }
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
code span.al { color: #CB4B16; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #D33682; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #5F8700; font-weight: bold; } /* ControlFlow */
code span.ch { color: #16801a; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #93A1A1; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { background-color: #f8edff; } /* DataType */
code span.dv { color: #D33682; } /* DecVal */
code span.er { color: #D30102; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #D33682; } /* Float */
code span.fu { } /* Function */
code span.im { color: #D70000} /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { font-weight: bold; } /* Keyword */
code span.op { font-weight: bold; } /* Operator */
code span.ot { font-weight: bold; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #16801a; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
+46 -68
View File
@@ -1,5 +1,5 @@
/*
This file is copied from
/*
This file is copied from
https://github.com/slotThe/slotThe.github.io/blob/main/css/sidenotes.css
with minor modifications made by Yu Cong.
@@ -19,148 +19,127 @@ body {
.sidenote,
.marginnote,
.marginnote-left {
position: relative;
float: right;
clear: right;
width: 36%;
margin-top: 0.45rem;
margin-right: -42%;
width: 36%;
margin-top: 0.3rem;
margin-bottom: 0;
padding: 0.85rem 1rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
backdrop-filter: blur(14px);
font-size: 0.8em;
line-height: 1.2;
vertical-align: baseline;
font-size: 0.82em;
line-height: 1.45;
position: relative;
text-align: left;
}
@media (max-width: 1200px) {
.sidenote,
.marginnote,
.marginnote-left {
width: 33%;
margin-right: -40%;
width: 33%;
}
}
.marginnote-left {
position: relative;
float: left;
clear: left;
width: 25%;
margin-left: -32%;
width: 25%;
position: relative;
text-align: right;
}
/* The first condition is for the case of a left-aligned layout
(on a smaller screen), and the second condition for a more centered
layout on a larger screen. */
@media (max-width: 1349px), (min-width: 1367px) and (max-width: 1620px) {
/* The first condition is for the case of a left-aligned layout (on a
smaller screen), and the second condition for a more centered layout
on a larger screen. It's a bit awkward, sadly :/ */
@media (max-width: 1349px) or ((min-width: 1367px) and (max-width: 1620px)) {
.marginnote-left {
width: 30%;
margin-left: -33%;
width: 30%;
}
}
.sidenote > :first-child,
.marginnote > :first-child,
.marginnote-left > :first-child {
margin-top: 0;
}
.sidenote > :last-child,
.marginnote > :last-child,
.marginnote-left > :last-child {
margin-bottom: 0;
}
.sidenote code {
font-size: 0.92em;
font-size: 0.94em;
}
/* Block code in side and margin notes gets a vertical scrollbar on some
browsers; disable that. */
/* For some reason, although only `overflow-x` is set in `default.css`,
block code in side and marginnotes gets a vertical (!) scrollbar no
matter what; disable that.
*/
div .marginnote pre,
div .sidenote pre {
margin: 0.75rem 0;
overflow-y: hidden;
}
.sidenote-number {
color: var(--color-link);
counter-increment: sidenote-counter;
font-family: "IosevkaC", sans-serif;
color: var(--color-link);
}
.sidenote-number:after,
.sidenote:before {
position: relative;
vertical-align: baseline;
color: var(--color-link);
font-family: "IosevkaC", sans-serif;
}
.sidenote-number:after {
content: counter(sidenote-counter);
top: -0.5rem;
font-size: 0.8rem;
font-weight: 700;
top: -0.5rem;
}
/* Properly position sidenote number and adjust position of sidenote
/* Properly position siednote number and adjust position of sidenote
paragraphs:
https://github.com/edwardtufte/tufte-css/issues/93#issuecomment-670695382
*/
.sidenote::before {
content: counter(sidenote-counter) " ";
position: absolute;
top: -0.55rem;
right: calc(100% + 0.5em);
font-size: 0.8rem;
font-weight: 700;
top: -0.55rem;
position: absolute;
right: calc(100% + 0.5em);
}
.sidenote p {
margin: 0.85em 0;
margin: 1em 0;
}
.sidenote p:first-child {
margin-top: 0;
}
.sidenote p:last-child {
margin-bottom: 0;
}
/* */
input.margin-toggle {
display: none;
}
label.sidenote-number {
display: inline-block;
max-height: 2rem;
max-height: 2rem; /* should be less than or equal to paragraph line-height */
}
label.margin-toggle:not(.sidenote-number) {
display: none;
color: var(--color-link);
font-family: "IosevkaC", sans-serif;
}
.iframe-wrapper {
position: relative;
height: 0;
padding-bottom: 56.25%; /* 16:9 */
padding-top: 25px;
padding-bottom: 56.25%;
overflow: hidden;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
box-shadow: var(--color-shadow-soft);
height: 0;
}
.iframe-wrapper iframe {
position: absolute;
inset: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
@media (max-width: 1200px) {
@@ -174,24 +153,23 @@ label.margin-toggle:not(.sidenote-number) {
display: none;
}
/* Linkify sidenotes iff they are clickable. */
/* Linkify sidenotes iff they are clickable */
.margin-toggle,
.sidenote-number:after {
color: var(--color-link);
text-decoration: underline;
text-decoration-color: var(--color-link-line);
text-decoration: none;
}
.margin-toggle:checked + .sidenote,
.margin-toggle:checked + .marginnote,
.margin-toggle:checked + .marginnote-left {
position: relative;
display: block;
float: left;
left: 1rem;
clear: both;
display: block;
width: 95%;
margin: 1rem 2.5%;
position: relative;
text-align: left;
}
+19 -5
View File
@@ -1,13 +1,22 @@
name: hakyll-blog
cabal-version: 2.4
name: hakysidian
version: 0.1.0.0
build-type: Simple
cabal-version: >= 1.10
data-files:
bib_style.csl
favicon.ico
css/*.css
fonts/*.otf
fonts/*.ttf
fonts/*.woff2
templates/*.html
executable site
executable hakysidian
hs-source-dirs: src
main-is: site.hs
other-modules: ChaoDoc, SideNoteHTML, Pangu
other-modules: ChaoDoc, SideNoteHTML, Pangu, Paths_hakysidian
autogen-modules: Paths_hakysidian
build-depends: base >= 4.18
, hakyll >= 4.15
, mtl >= 2.2.2
@@ -20,7 +29,12 @@ executable site
-- , process
-- , regex-compat
, array
, directory
, filepath
, process
, time
, wai-app-static
, warp
-- , ghc-syntax-highlighter
-- , blaze-html >= 0.9
, megaparsec
@@ -34,4 +48,4 @@ executable site
-Wno-unsafe
-Wno-prepositive-qualified-module
-O2 -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010
default-language: Haskell2010
+8 -7
View File
@@ -1,5 +1,6 @@
COMMANDS := build watch rebuild clean
BIN := hakysidian
.PHONY: $(COMMANDS), publish
# Set the default goal, so running 'make' without arguments will run 'make build'.
@@ -7,20 +8,20 @@ COMMANDS := build watch rebuild clean
# ---
$(COMMANDS): site
@echo "Running command: ./site $@"
-@./site $@
$(COMMANDS): $(BIN)
@echo "Running command: ./$(BIN) $@"
-@./$(BIN) $@
# --- Rules ---
# using relative symlinks should be fine since everything only works at ./
site: src/site.hs src/ChaoDoc.hs
cabal build
ln -sf "$(shell cabal list-bin exe:site)" site
$(BIN): src/site.hs src/ChaoDoc.hs
cabal build exe:hakysidian
ln -sf "$(shell cabal list-bin exe:hakysidian)" $(BIN)
# move from katex to mathjax
# katex_cli:
# cd katex_rust_fork && cargo build --release
# ln -sf ./katex_rust_fork/target/release/katex_cli katex_cli
# ln -sf ./katex_rust_fork/target/release/katex_cli katex_cli
+105 -5
View File
@@ -1,6 +1,106 @@
things don't work:
# hakysidian
1. equation labels & paragraph labels
2. pandoc does not support mathtools: <https://github.com/jgm/texmath/issues/249>
3. cross document refs
4.
`hakysidian` is a static site generator for note projects.
It is built on Hakyll, but packaged as a reusable CLI so you can run the same site generator across multiple note repositories without copying shared assets around. The executable bundles its shared `css/`, `fonts/`, `templates/`, `favicon.ico`, and `bib_style.csl` files with Cabal, then reads project-specific content from the current working directory.
## What It Expects
Run `hakysidian` inside a project directory with this layout:
```text
your-project/
├── notes/
│ ├── first-note.md
│ └── another-note.md
├── reference.bib
├── math-macros.md
└── images/ # optional
```
Required inputs:
- `notes/`: markdown notes to compile.
- `reference.bib`: bibliography used by Pandoc citeproc.
- `math-macros.md`: math macro definitions prepended before note parsing.
Optional inputs:
- `images/`: copied into the generated site as-is.
Shared assets are not required in each project. They come from the installed `hakysidian` package.
## Output
By default, `hakysidian` writes:
- `_site/`: generated site output.
- `_cache/`: Hakyll cache and temporary files.
Note pages use clean URLs. For example:
```text
notes/graph.md -> _site/notes/graph/index.html
```
## Install
From this repository:
```bash
cabal build exe:hakysidian
cabal install exe:hakysidian
```
## Commands
The CLI mirrors the common Hakyll workflow:
```bash
hakysidian build
hakysidian clean
hakysidian rebuild
hakysidian watch
```
`watch` also supports:
```bash
hakysidian watch --host 127.0.0.1 --port 8000
hakysidian watch --no-server
```
What each command does:
- `build`: incremental site build.
- `clean`: removes generated output and cache.
- `rebuild`: clears output/cache and builds from scratch.
- `watch`: shows an in-place terminal dashboard, watches project inputs, and rebuilds automatically on change.
## Watch Mode
`watch` tracks:
- `notes/**`
- `reference.bib`
- `math-macros.md`
- `images/**`
The watch UI:
- uses the terminals current size to keep the dashboard within the visible screen,
- keeps recent build output in a bounded activity pane,
- avoids scrolling raw Hakyll logs through the terminal,
- can start a local preview server unless `--no-server` is passed.
## Notes Format
This generator is opinionated toward the current note pipeline in this repository:
- Markdown is parsed with Pandoc and custom theorem/callout handling.
- math is rendered with MathML. looks good in firefox
- sidenotes are supported
- spacing between CJK chars and ascii is automatically handled by a filter.
- Citations are processed through `reference.bib` and the bundled `bib_style.csl`.
- `math-macros.md` is injected before parsing so note content and theorem titles can use the same macros.
- Notes are rendered with the bundled templates and stylesheet set.
+20 -33
View File
@@ -124,9 +124,9 @@ preprocessTheorems (Div attr xs)
attr' = addAttr attr "type" theoremType
preprocessTheorems x = return x
theoremFilter :: Pandoc -> Pandoc
theoremFilter doc =
walk makeTheorem $
theoremFilter :: Text -> Pandoc -> Pandoc
theoremFilter mathMacros doc =
walk (makeTheorem mathMacros) $
autorefFilter $
evalState (walkM preprocessTheorems normalizedDoc) 1
where
@@ -171,24 +171,13 @@ autorefFilter x = walk (autoref links) x
where
links = query theoremIndex x
-- processCitations works on AST. If you want to use citations in theorem name,
-- then you need to convert citations there to AST as well and then use processCitations\
-- Thus one need to apply the theorem filter first.
-- autoref still does not work.
mathMacros :: Text
mathMacros = unsafePerformIO (pack <$> readFile "math-macros.md")
{-# NOINLINE mathMacros #-}
prependMacros :: Text -> Text -> Text
prependMacros macros body = macros <> "\n\n" <> body
prependMathMacros :: Text -> Text
prependMathMacros = prependMacros mathMacros
thmNamePandoc :: Text -> Pandoc
thmNamePandoc x =
thmNamePandoc :: Text -> Text -> Pandoc
thmNamePandoc mathMacros x =
fromRight (Pandoc nullMeta []) . runPure $
readMarkdown chaoDocRead (prependMathMacros x)
readMarkdown chaoDocRead (prependMacros mathMacros x)
obsidianTheoremFilter :: Pandoc -> Pandoc
obsidianTheoremFilter = attachStandaloneLabels . walk rewriteObsidianBlockQuote
@@ -390,8 +379,8 @@ unsnoc (x : xs) = do
(prefix, lastElem) <- unsnoc xs
return (x : prefix, lastElem)
makeTheorem :: Block -> Block
makeTheorem (Div attr xs)
makeTheorem :: Text -> Block -> Block
makeTheorem mathMacros (Div attr xs)
| isNothing t = Div attr xs
| otherwise = Div (addClass attr "theorem-environment") (Plain [header] : xs)
where
@@ -408,26 +397,23 @@ makeTheorem (Div attr xs)
nametext =
if isNothing name
then Str ""
else Span (addClass nullAttr "name") (pandocToInline $ thmNamePandoc $ fromJust name)
makeTheorem x = x
else Span (addClass nullAttr "name") (pandocToInline $ thmNamePandoc mathMacros $ fromJust name)
makeTheorem _ x = x
-- bib from https://github.com/chaoxu/chaoxu.github.io/tree/develop
cslFile :: String
cslFile = "bib_style.csl"
bibFile :: String
bibFile :: T.Text
bibFile = "reference.bib"
chaoDocPandocCompiler :: Compiler (Item Pandoc)
chaoDocPandocCompiler = do
chaoDocPandocCompiler :: FilePath -> Compiler (Item Pandoc)
chaoDocPandocCompiler cslPath = do
macros <- T.pack <$> loadBody "math-macros.md"
void (loadBody "reference.bib" :: Compiler String)
body <- getResourceBody
let bodyWithMacros =
fmap (T.unpack . prependMacros macros . T.pack) body
myReadPandocBiblio chaoDocRead (T.pack cslFile) (T.pack bibFile) myFilter bodyWithMacros
myReadPandocBiblio chaoDocRead (T.pack cslPath) bibFile (myFilter macros) bodyWithMacros
chaoDocCompiler :: Compiler (Item String)
chaoDocCompiler = chaoDocPandocCompiler <&> writePandocWith chaoDocWrite
chaoDocCompiler :: FilePath -> Compiler (Item String)
chaoDocCompiler cslPath = chaoDocPandocCompiler cslPath <&> writePandocWith chaoDocWrite
addMeta :: T.Text -> MetaValue -> Pandoc -> Pandoc
addMeta name value (Pandoc meta a) =
@@ -465,8 +451,9 @@ myReadPandocBiblio ropt csl biblio pdfilter item = do
-- let a x = itemSetBody (pandoc' x)
return $ fmap (const pandoc') item
myFilter :: Pandoc -> Pandoc
myFilter = usingSideNotesHTML chaoDocWrite . theoremFilter . panguFilter . displayMathFilter
myFilter :: Text -> Pandoc -> Pandoc
myFilter mathMacros =
usingSideNotesHTML chaoDocWrite . theoremFilter mathMacros . panguFilter . displayMathFilter
-- pangu filter
lastChar :: Inline -> Maybe Char
+677 -167
View File
@@ -1,230 +1,740 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE ViewPatterns #-}
import ChaoDoc
import Data.List (sortOn)
import Control.Concurrent (forkIO, threadDelay)
import Control.Exception (SomeException, bracket_, try)
import Control.Monad (filterM, unless, void)
import Data.Char (isSpace)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Kind (Type)
import Data.List (intercalate, isPrefixOf, sort, sortOn)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.String (fromString)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.Time.LocalTime (getZonedTime)
import Hakyll
import Hakyll.Core.Runtime (RunMode (RunModeNormal))
import qualified Network.Wai.Handler.Warp as Warp
import qualified Paths_hakysidian as Paths
import System.Directory
( canonicalizePath,
doesDirectoryExist,
doesFileExist,
getCurrentDirectory,
getModificationTime,
listDirectory
)
import System.Environment (getArgs, getExecutablePath, lookupEnv)
import System.Exit (ExitCode (..), die, exitSuccess, exitWith)
import System.FilePath
import Text.Pandoc
import Network.Wai.Application.Static (staticApp)
import System.IO
( BufferMode (NoBuffering),
hFlush,
hGetBuffering,
hIsTerminalDevice,
hSetBuffering,
stdout
)
import System.Process (CreateProcess (cwd), proc, readCreateProcessWithExitCode)
import Text.Pandoc (HTMLMathMethod (MathML), WriterOptions (..), compileTemplate)
import Text.Read (readMaybe)
--------------------------------------------------------------------------------
notesPattern :: Pattern
notesPattern = fromGlob "notes/**"
bundledCssFiles :: [FilePath]
bundledCssFiles =
[ "css/fonts.css",
"css/default.css",
"css/pygentize.css",
"css/chao-theorems.css",
"css/sidenotes.css"
]
bundledFontFiles :: [FilePath]
bundledFontFiles =
[ "fonts/Lato-BoldItalic.woff2",
"fonts/IosevkaCustom-Bold.woff2",
"fonts/IosevkaCustom-Regular.woff2",
"fonts/Lato-Bold.woff2",
"fonts/Lato-Regular.woff2",
"fonts/IosevkaCustom-Italic.woff2",
"fonts/LeteSansMath.woff2",
"fonts/LeteSansMath-Bold.woff2",
"fonts/Lato-Italic.woff2"
]
bundledTemplateFiles :: [FilePath]
bundledTemplateFiles =
[ "templates/head.html",
"templates/note.html",
"templates/notes.html",
"templates/notes-list.html",
"templates/index.html",
"templates/navbar.html"
]
type WatchSettings :: Type
data WatchSettings = WatchSettings
{ watchHost :: String,
watchPort :: Int,
watchServerEnabled :: Bool
}
type CliCommand :: Type
data CliCommand
= BuildCommand
| CleanCommand
| HelpCommand
| RebuildCommand
| WatchCommand WatchSettings
type FileSnapshot :: Type
type FileSnapshot = M.Map FilePath UTCTime
type ServerStatus :: Type
data ServerStatus
= ServerDisabled
| ServerStarting
| ServerRunning
| ServerFailed String
deriving stock (Eq)
type DashboardState :: Type
data DashboardState = DashboardState
{ dashboardStatus :: String,
dashboardLastChange :: String,
dashboardLastBuild :: String,
dashboardLogLines :: [String]
}
deriving stock (Eq)
type TerminalSize :: Type
data TerminalSize = TerminalSize
{ terminalRows :: Int,
terminalCols :: Int
}
deriving stock (Eq)
--------------------------------------------------------------------------------
-- https://www.rohanjain.in/hakyll-clean-urls/
cleanRoute :: Routes
cleanRoute = customRoute createIndexRoute
where
createIndexRoute ident = takeDirectory p </> takeBaseName p </> "index.html"
createIndexRoute ident = takeDirectory path </> takeBaseName path </> "index.html"
where
p = toFilePath ident
path = toFilePath ident
cleanIndexHtmls :: Item String -> Compiler (Item String)
cleanIndexHtmls = return . fmap (replaceAll pattern replacement)
where
pattern :: String = "/index.html"
replacement :: String -> String = const "/"
pattern :: String
pattern = "/index.html"
replacement :: String -> String
replacement = const "/"
loadNoteLinks :: Compiler [Item String]
loadNoteLinks = do
noteIds <- sortOn toFilePath <$> getMatches notesPattern
pure [Item noteId "" | noteId <- noteIds]
--------------------------------------------------------------------------------
config :: Configuration
config =
siteConfiguration :: FilePath -> Configuration
siteConfiguration projectRoot =
defaultConfiguration
{ ignoreFile = \path ->
ignoreFile defaultConfiguration path
|| ".git" `elem` splitDirectories (normalise path)
{ destinationDirectory = projectRoot </> "_site",
storeDirectory = projectRoot </> "_cache",
tmpDirectory = projectRoot </> "_cache" </> "tmp",
providerDirectory = projectRoot,
ignoreFile = ignoreProjectFile (ignoreFile defaultConfiguration),
watchIgnore = ignoreProjectFile (watchIgnore defaultConfiguration)
}
ignoreProjectFile :: (FilePath -> Bool) -> FilePath -> Bool
ignoreProjectFile defaultIgnore path = defaultIgnore path || ignoredProjectPath path
ignoredProjectPath :: FilePath -> Bool
ignoredProjectPath path =
any (`elem` [".git", ".obsidian"]) (splitDirectories (normalise path))
--------------------------------------------------------------------------------
main :: IO ()
main = hakyllWith config $ do
main = do
args <- getArgs
projectRoot <- canonicalizePath =<< getCurrentDirectory
let config = siteConfiguration projectRoot
cslPath <- Paths.getDataFileName "bib_style.csl"
case parseCliCommand config args of
Left err -> die (err <> "\n\n" <> usageText)
Right HelpCommand -> putStrLn usageText >> exitSuccess
Right CleanCommand ->
exitWith =<< runSiteCommand config cleanOptions cslPath
Right BuildCommand -> do
validateProject projectRoot
exitWith =<< runSiteCommand config buildOptions cslPath
Right RebuildCommand -> do
validateProject projectRoot
exitWith =<< runSiteCommand config rebuildOptions cslPath
Right (WatchCommand watchSettings) -> do
validateProject projectRoot
exitWith =<< runWatch projectRoot config cslPath watchSettings
usageText :: String
usageText =
unlines
[ "usage: hakysidian [build|clean|rebuild|watch [--host HOST] [--port PORT] [--no-server]]",
"",
"Run inside a project directory containing notes/, reference.bib, math-macros.md, and optional images/."
]
parseCliCommand :: Configuration -> [String] -> Either String CliCommand
parseCliCommand config args
| any (`elem` ["-h", "--help"]) args = Right HelpCommand
| otherwise = case args of
[] -> Right BuildCommand
["build"] -> Right BuildCommand
["clean"] -> Right CleanCommand
["rebuild"] -> Right RebuildCommand
"watch" : rest -> Right (WatchCommand (parseWatchSettings config rest))
command : _ -> Left ("Unknown command: " <> command)
validateProject :: FilePath -> IO ()
validateProject projectRoot = do
notesExists <- doesDirectoryExist (projectRoot </> "notes")
bibExists <- doesFileExist (projectRoot </> "reference.bib")
macrosExists <- doesFileExist (projectRoot </> "math-macros.md")
let missing :: [String]
missing =
[ "notes/"
| not notesExists
]
++ [ "reference.bib"
| not bibExists
]
++ [ "math-macros.md"
| not macrosExists
]
unless (null missing) $
die $
unlines $
"hakysidian is missing required project inputs:" :
map (" - " ++) missing
initialDashboardState :: DashboardState
initialDashboardState =
DashboardState
{ dashboardStatus = "starting",
dashboardLastChange = "waiting for first build",
dashboardLastBuild = "pending",
dashboardLogLines =
[ "watcher ready",
"watching notes/, reference.bib, math-macros.md, images/ (optional)"
]
}
parseWatchSettings :: Configuration -> [String] -> WatchSettings
parseWatchSettings config args =
WatchSettings
{ watchHost = fromMaybe (previewHost config) (extractOptionValue "--host" args),
watchPort =
fromMaybe (previewPort config) $
extractOptionValue "--port" args >>= readMaybe,
watchServerEnabled = "--no-server" `notElem` args
}
watchUrl :: WatchSettings -> Maybe String
watchUrl settings
| watchServerEnabled settings =
Just $
"http://"
++ displayHost (watchHost settings)
++ ":"
++ show (watchPort settings)
++ "/"
| otherwise = Nothing
where
displayHost :: String -> String
displayHost "0.0.0.0" = "127.0.0.1"
displayHost hostName = hostName
extractOptionValue :: String -> [String] -> Maybe String
extractOptionValue option = go
where
optionPrefix = option ++ "="
go :: [String] -> Maybe String
go [] = Nothing
go [arg]
| optionPrefix `isPrefixOf` arg = Just (drop (length optionPrefix) arg)
| otherwise = Nothing
go (arg : value : rest)
| arg == option = Just value
| optionPrefix `isPrefixOf` arg = Just (drop (length optionPrefix) arg)
| otherwise = go (value : rest)
withWatchTui :: IO a -> IO a
withWatchTui action = do
interactive <- hIsTerminalDevice stdout
if interactive
then do
originalBuffering <- hGetBuffering stdout
bracket_
(do
hSetBuffering stdout NoBuffering
putStr "\ESC[?1049h\ESC[2J\ESC[H\ESC[?25l"
hFlush stdout)
(do
putStr "\ESC[0m\ESC[?25h\ESC[?1049l"
hFlush stdout
hSetBuffering stdout originalBuffering)
action
else action
renderWatchDashboard ::
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
FilePath ->
Configuration ->
WatchSettings ->
IORef ServerStatus ->
DashboardState ->
IO ()
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard = do
terminalSize <- getTerminalSize
serverStatus <- readIORef serverStatusRef
previousRenderState <- readIORef renderStateRef
let currentRenderState = Just (terminalSize, serverStatus, dashboard)
unless (currentRenderState == previousRenderState) do
let rows = max 1 (terminalRows terminalSize)
cols = max 4 (terminalCols terminalSize)
border = "+" ++ replicate (cols - 2) '-' ++ "+"
infoRows =
[ dashboardRow cols ("Project : " ++ projectRoot),
-- dashboardRow cols ("Output : " ++ destinationDirectory config),
dashboardRow cols ("Preview : " ++ renderServerStatus watchSettings serverStatus),
-- dashboardRow cols "Watch : notes/, reference.bib, math-macros.md, images/ (optional)",
-- dashboardRow cols ("Change : " ++ dashboardLastChange dashboard),
dashboardRow cols ("Build : " ++ dashboardLastBuild dashboard)
]
headerRows =
[ border,
dashboardTitleRow cols "hakysidian watch" (dashboardStatus dashboard),
border
]
++ infoRows
++ [border, dashboardRow cols "Recent activity", border]
footerRows = [border]
availableLogRows = max 1 (rows - length headerRows - length footerRows)
logRows =
map (dashboardRow cols) $
padRows availableLogRows $
takeLast availableLogRows (dashboardLogLines dashboard)
screenRows = take rows (headerRows ++ logRows ++ footerRows)
putStr "\ESC[2J\ESC[H"
putStr (intercalate "\n" screenRows)
hFlush stdout
writeIORef renderStateRef currentRenderState
dashboardTitleRow :: Int -> String -> String -> String
dashboardTitleRow width leftText rightText =
dashboardFramedRow width (leftText ++ spacer ++ clippedRight)
where
usableWidth = max 1 (width - 4)
rightWidth = min (usableWidth `div` 3) (length rightText)
clippedRight =
if null rightText
then ""
else ellipsize rightWidth rightText
leftWidth = max 1 (usableWidth - length clippedRight - 1)
clippedLeft = ellipsize leftWidth leftText
spacer
| null clippedRight = ""
| otherwise = replicate (max 1 (usableWidth - length clippedLeft - length clippedRight)) ' '
dashboardRow :: Int -> String -> String
dashboardRow width = dashboardFramedRow width
dashboardFramedRow :: Int -> String -> String
dashboardFramedRow width content =
"| " ++ padRight usableWidth (ellipsize usableWidth content) ++ " |"
where
usableWidth = max 1 (width - 4)
padRight :: Int -> String -> String
padRight width text = text ++ replicate (max 0 (width - length text)) ' '
padRows :: Int -> [String] -> [String]
padRows count rows =
rows ++ replicate (max 0 (count - length rows)) ""
takeLast :: Int -> [a] -> [a]
takeLast count xs = drop (max 0 (length xs - count)) xs
ellipsize :: Int -> String -> String
ellipsize width text
| width <= 0 = ""
| length text <= width = text
| width <= 3 = take width text
| otherwise = take (width - 3) text ++ "..."
renderServerStatus :: WatchSettings -> ServerStatus -> String
renderServerStatus watchSettings serverStatus = case serverStatus of
ServerDisabled -> "disabled (--no-server)"
ServerStarting -> maybe "starting" (++ " (starting)") (watchUrl watchSettings)
ServerRunning -> fromMaybe "running" (watchUrl watchSettings)
ServerFailed err ->
"failed: " ++ err
getTerminalSize :: IO TerminalSize
getTerminalSize = do
sttySize <- queryTerminalSize
case sttySize of
Just terminalSize -> pure terminalSize
Nothing -> do
rows <- maybe 24 id . (>>= readMaybe) <$> lookupEnv "LINES"
cols <- maybe 80 id . (>>= readMaybe) <$> lookupEnv "COLUMNS"
pure (TerminalSize rows cols)
queryTerminalSize :: IO (Maybe TerminalSize)
queryTerminalSize = do
result <-
try $
readCreateProcessWithExitCode
(proc "sh" ["-c", "stty size </dev/tty"])
"" :: IO (Either SomeException (ExitCode, String, String))
pure $ do
(exitCode, stdoutText, _) <- either (const Nothing) Just result
case exitCode of
ExitSuccess -> case words stdoutText of
[rowsText, colsText] -> do
rows <- readMaybe rowsText
cols <- readMaybe colsText
Just (TerminalSize rows cols)
_ -> Nothing
ExitFailure _ -> Nothing
watchTimestamp :: IO String
watchTimestamp = formatTime defaultTimeLocale "%H:%M:%S" <$> getZonedTime
trimTrailingSpace :: String -> String
trimTrailingSpace = reverse . dropWhile isSpace . reverse
normalizeLogLines :: String -> String -> [String]
normalizeLogLines stdoutText stderrText =
filter (not . null) $
map trimTrailingSpace $
lines (stdoutText ++ if null stderrText then "" else "\n" ++ stderrText)
appendLogBatch :: DashboardState -> String -> String -> [String] -> DashboardState
appendLogBatch dashboard title timestamp buildLines =
dashboard
{ dashboardLogLines =
takeLast 200 $
dashboardLogLines dashboard
++ ("[" ++ timestamp ++ "] " ++ title)
: map (" " ++) buildLines
}
runCapturedSiteCommand :: FilePath -> String -> IO (ExitCode, [String])
runCapturedSiteCommand projectRoot command = do
executablePath <- getExecutablePath
(exitCode, stdoutText, stderrText) <-
readCreateProcessWithExitCode
(proc executablePath [command]) {cwd = Just projectRoot}
""
pure (exitCode, normalizeLogLines stdoutText stderrText)
buildOptions :: Options
buildOptions = Options {verbosity = False, optCommand = Build RunModeNormal}
cleanOptions :: Options
cleanOptions = Options {verbosity = False, optCommand = Clean}
rebuildOptions :: Options
rebuildOptions = Options {verbosity = False, optCommand = Rebuild}
runSiteCommand :: Configuration -> Options -> FilePath -> IO ExitCode
runSiteCommand config options cslPath =
hakyllWithExitCodeAndArgs config options (siteRules cslPath)
runWatch :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
runWatch projectRoot config _cslPath watchSettings =
withWatchTui do
serverStatusRef <- newIORef initialServerStatus
renderStateRef <- newIORef Nothing
startPreviewServer config watchSettings serverStatusRef
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef initialDashboardState
(_, initialDashboard) <-
runWatchBuild
"build"
"initial build"
"initial build"
projectRoot
config
watchSettings
renderStateRef
serverStatusRef
initialDashboardState
initialSnapshot <- snapshotInputs projectRoot
watchLoop renderStateRef serverStatusRef initialSnapshot initialDashboard
where
initialServerStatus
| watchServerEnabled watchSettings = ServerStarting
| otherwise = ServerDisabled
watchLoop ::
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
IORef ServerStatus ->
FileSnapshot ->
DashboardState ->
IO ExitCode
watchLoop renderStateRef serverStatusRef previousSnapshot dashboard = do
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard
threadDelay 1000000
nextSnapshot <- snapshotInputs projectRoot
if nextSnapshot == previousSnapshot
then watchLoop renderStateRef serverStatusRef previousSnapshot dashboard
else do
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
command :: String
command =
if any (`M.notMember` nextSnapshot) (M.keys previousSnapshot)
then "rebuild"
else "build"
changeSummary = intercalate ", " changedFiles
(_, nextDashboard) <-
runWatchBuild
command
command
changeSummary
projectRoot
config
watchSettings
renderStateRef
serverStatusRef
dashboard
watchLoop renderStateRef serverStatusRef nextSnapshot nextDashboard
renderBuildResult :: ExitCode -> String
renderBuildResult ExitSuccess = "success"
renderBuildResult (ExitFailure code) = "failed (" ++ show code ++ ")"
runWatchBuild ::
String ->
String ->
String ->
FilePath ->
Configuration ->
WatchSettings ->
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
IORef ServerStatus ->
DashboardState ->
IO (ExitCode, DashboardState)
runWatchBuild command label changeSummary projectRoot config watchSettings renderStateRef serverStatusRef dashboard = do
startedAt <- watchTimestamp
let runningDashboard =
dashboard
{ dashboardStatus = "building (" ++ label ++ ")",
dashboardLastChange = changeSummary,
dashboardLastBuild = "running since " ++ startedAt
}
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef runningDashboard
(exitCode, buildLines) <- runCapturedSiteCommand projectRoot command
finishedAt <- watchTimestamp
let loggedDashboard =
appendLogBatch runningDashboard (label ++ ": " ++ changeSummary) finishedAt buildLines
completedDashboard =
loggedDashboard
{ dashboardStatus =
if exitCode == ExitSuccess
then "watching"
else "watching after failed " ++ label,
dashboardLastBuild =
renderBuildResult exitCode
++ " at "
++ finishedAt
++ " via "
++ label
}
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef completedDashboard
pure (exitCode, completedDashboard)
startPreviewServer :: Configuration -> WatchSettings -> IORef ServerStatus -> IO ()
startPreviewServer config watchSettings serverStatusRef
| watchServerEnabled watchSettings =
void $
forkIO $
do
result <-
(try $
Warp.runSettings settings $
staticApp $
previewSettings config (destinationDirectory config)) ::
IO (Either SomeException ())
case result of
Left err -> writeIORef serverStatusRef (ServerFailed (show err))
Right () -> pure ()
| otherwise = writeIORef serverStatusRef ServerDisabled
where
settings =
Warp.setBeforeMainLoop (writeIORef serverStatusRef ServerRunning) $
Warp.setPort (watchPort watchSettings) $
Warp.setHost (fromString (watchHost watchSettings)) $
Warp.defaultSettings
snapshotInputs :: FilePath -> IO FileSnapshot
snapshotInputs projectRoot = do
inputFiles <- trackedInputs projectRoot
entries <- traverse toSnapshotEntry inputFiles
pure (M.fromList entries)
where
toSnapshotEntry :: FilePath -> IO (FilePath, UTCTime)
toSnapshotEntry path = do
modifiedAt <- getModificationTime path
pure (makeRelative projectRoot path, modifiedAt)
trackedInputs :: FilePath -> IO [FilePath]
trackedInputs projectRoot = do
requiredFiles <-
filterM
doesFileExist
[ projectRoot </> "reference.bib",
projectRoot </> "math-macros.md"
]
noteFiles <- trackedFilesIn (projectRoot </> "notes")
imageFiles <- trackedFilesIn (projectRoot </> "images")
pure (sort (requiredFiles ++ noteFiles ++ imageFiles))
trackedFilesIn :: FilePath -> IO [FilePath]
trackedFilesIn root = do
exists <- doesDirectoryExist root
if exists
then do
entries <- listDirectory root
fmap concat $
traverse
( \name -> do
let path = root </> name
isDir <- doesDirectoryExist path
if isDir
then trackedFilesIn path
else pure [path]
)
entries
else pure []
diffSnapshots :: FileSnapshot -> FileSnapshot -> [FilePath]
diffSnapshots previousSnapshot nextSnapshot =
sort
[ path
| path <- M.keys (M.union previousSnapshot nextSnapshot),
M.lookup path previousSnapshot /= M.lookup path nextSnapshot
]
--------------------------------------------------------------------------------
siteRules :: FilePath -> Rules ()
siteRules cslPath = do
match "images/**" $ do
route idRoute
compile copyFileCompiler
match "math-macros.md" $ compile getResourceBody
match "math-macros.md" $
compile getResourceBody
match "fonts/*.woff2" $ do
route idRoute
compile copyFileCompiler
match "reference.bib" $
compile getResourceBody
match "favicon.ico" $ do
route idRoute
compile copyFileCompiler
mapM_ createBundledCss bundledCssFiles
mapM_ createBundledCopy bundledFontFiles
createBundledCopy "favicon.ico"
mapM_ createBundledTemplate bundledTemplateFiles
-- match "404.html" $ do
-- route cleanRoute
-- compile copyFileCompiler
match "css/*" $ do
route idRoute
compile compressCssCompiler
-- match "about.md" $ do
-- route cleanRoute
-- compile $
-- chaoDocCompiler
-- >>= loadAndApplyTemplate "templates/about.html" defaultContext
-- >>= relativizeUrls
-- -- build up tags
-- tags <- buildTags "posts/*" (fromCapture "tags/*.html")
-- tagsRules tags $ \tag pattern -> do
-- let title = "Posts tagged \"" ++ tag ++ "\""
-- route cleanRoute
-- compile $ do
-- posts <- recentFirst =<< loadAll pattern
-- let ctx =
-- constField "title" title
-- `mappend` listField "posts" (postCtxWithTags tags) (return posts)
-- `mappend` defaultContext
-- makeItem ""
-- >>= loadAndApplyTemplate "templates/tag.html" ctx
-- >>= loadAndApplyTemplate "templates/default.html" ctx
-- >>= relativizeUrls
-- create ["tags.html"] $ do
-- route cleanRoute
-- compile $ do
-- makeItem ""
-- >>= loadAndApplyTemplate "templates/tags.html" (defaultCtxWithTags tags)
-- >>= loadAndApplyTemplate "templates/default.html" (defaultCtxWithTags tags)
-- match "posts/*" $ do
-- route cleanRoute
-- compile $ do
-- tocCtx <- getTocCtx (postCtxWithTags tags)
-- chaoDocCompiler
-- >>= loadAndApplyTemplate "templates/post.html" tocCtx
-- >>= loadAndApplyTemplate "templates/default.html" tocCtx
-- >>= relativizeUrls
-- -- >>= katexFilter
match "notes/*" $ do
match notesPattern $ do
route cleanRoute
compile $ do
tocCtx <- getTocCtx defaultContext
chaoDocCompiler
notes <- loadNoteLinks
tocCtx <- getTocCtx cslPath (listField "notes" defaultContext (pure notes) <> defaultContext)
chaoDocCompiler cslPath
>>= loadAndApplyTemplate "templates/note.html" tocCtx
>>= relativizeUrls
create ["index.html"] $ do
route idRoute
compile $ do
notes <- sortOn (toFilePath . itemIdentifier) <$> loadAll "notes/*"
notes <- loadNoteLinks
let notesCtx =
listField "posts" defaultContext (return notes)
`mappend` constField "title" "Notes"
`mappend` defaultContext
listField "notes" defaultContext (pure notes)
<> constField "title" "Notes"
<> defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/notes.html" notesCtx
>>= loadAndApplyTemplate "templates/index.html" notesCtx
>>= relativizeUrls
>>= cleanIndexHtmls
-- create ["archive.html"] $ do
-- route cleanRoute
-- compile $ do
-- posts <- recentFirst =<< loadAll "posts/*"
-- let archiveCtx =
-- listField "posts" postCtx (return posts)
-- `mappend` constField "title" "Archives"
-- `mappend` defaultContext
-- makeItem ""
-- >>= loadAndApplyTemplate "templates/archive.html" archiveCtx
-- >>= loadAndApplyTemplate "templates/index.html" archiveCtx
-- >>= relativizeUrls
-- >>= cleanIndexHtmls
createBundledCss :: FilePath -> Rules ()
createBundledCss relPath = create [fromFilePath relPath] $ do
route idRoute
compile bundledCssCompiler
-- create ["draft.html"] $ do
-- route cleanRoute
-- compile $ do
-- posts <- recentFirst =<< loadAll "posts/*"
-- let draftCtx =
-- listField "posts" postCtx (return posts)
-- `mappend` constField "title" "Drafts"
-- `mappend` defaultContext
-- makeItem ""
-- >>= loadAndApplyTemplate "templates/draft.html" draftCtx
-- >>= loadAndApplyTemplate "templates/index.html" draftCtx
-- >>= relativizeUrls
-- >>= cleanIndexHtmls
createBundledCopy :: FilePath -> Rules ()
createBundledCopy relPath = create [fromFilePath relPath] $ do
route idRoute
compile bundledCopyCompiler
-- match "index.html" $ do
-- route idRoute
-- compile $ do
-- posts <- fmap (take 25) . recentFirst =<< loadAll "posts/*"
-- let indexCtx =
-- listField "posts" postCtx (return posts)
-- `mappend` defaultContext
-- getResourceBody
-- >>= applyAsTemplate indexCtx
-- >>= loadAndApplyTemplate "templates/index.html" indexCtx
-- >>= relativizeUrls
-- >>= cleanIndexHtmls
createBundledTemplate :: FilePath -> Rules ()
createBundledTemplate relPath =
create [fromFilePath relPath] $
compile bundledTemplateCompiler
match "templates/*" $ compile templateBodyCompiler
bundledAssetPath :: Compiler FilePath
bundledAssetPath = do
ident <- getUnderlying
unsafeCompiler $ Paths.getDataFileName (toFilePath ident)
-- https://robertwpearce.com/hakyll-pt-2-generating-a-sitemap-xml-file.html
-- create ["sitemap.xml"] $ do
-- route idRoute
-- compile $ do
-- posts <- recentFirst =<< loadAll "posts/*"
-- singlePages <- loadAll (fromList ["about.md"])
-- let pages = posts <> singlePages
-- sitemapCtx =
-- constField "root" root
-- <> listField "pages" postCtx (return pages) -- here
-- makeItem ""
-- >>= loadAndApplyTemplate "templates/sitemap.xml" sitemapCtx
bundledTextCompiler :: Compiler (Item String)
bundledTextCompiler = do
assetPath <- bundledAssetPath
contents <- unsafeCompiler (readFile assetPath)
makeItem contents
bundledCssCompiler :: Compiler (Item String)
bundledCssCompiler = fmap compressCss <$> bundledTextCompiler
bundledCopyCompiler :: Compiler (Item CopyFile)
bundledCopyCompiler = do
assetPath <- bundledAssetPath
makeItem (CopyFile assetPath)
bundledTemplateCompiler :: Compiler (Item Template)
bundledTemplateCompiler = do
item <- bundledTextCompiler
template <- compileTemplateItem item
pure (itemSetBody template item)
--------------------------------------------------------------------------------
-- isZhField :: Context String
-- isZhField = boolFieldM "isZh" isZh
-- where
-- isZh :: Item String -> Compiler Bool
-- isZh item = do
-- maybeLang <- getMetadataField (itemIdentifier item) "lang"
-- return (maybeLang == Just "zh")
-- postCtx :: Context String
-- postCtx =
-- dateField "date" "%B %e, %Y"
-- <> dateField "date" "%Y-%m-%d"
-- <> isZhField
-- <> defaultContext
-- postCtxWithTags :: Tags -> Context String
-- postCtxWithTags tags = tagsField "tags" tags `mappend` postCtx
-- defaultCtxWithTags :: Tags -> Context String
-- defaultCtxWithTags tags = listField "tags" tagsCtx getAllTags <> defaultContext
-- where
-- getAllTags :: Compiler [Item (String, [Identifier])]
-- getAllTags = pure . map mkItem $ tagsMap tags
-- where
-- mkItem :: (String, [Identifier]) -> Item (String, [Identifier])
-- mkItem x@(t, _) = Item (tagsMakeId tags t) x
-- tagsCtx =
-- listFieldWith "posts" (postCtxWithTags tags) getPosts
-- <> metadataField
-- <> urlField "url"
-- <> pathField "path"
-- <> titleField "title"
-- <> missingField
-- where
-- getPosts ::
-- Item (String, [Identifier]) ->
-- Compiler [Item String]
-- getPosts (itemBody -> (_, is)) = mapM load is
-- toc from https://github.com/slotThe/slotThe.github.io
getTocCtx :: Context a -> Compiler (Context a)
getTocCtx ctx = do
getTocCtx :: FilePath -> Context a -> Compiler (Context a)
getTocCtx cslPath ctx = do
noToc <- (Just "true" ==) <$> (getUnderlying >>= (`getMetadataField` "no-toc"))
writerOpts <- mkTocWriter defaultHakyllWriterOptions
toc <- writePandocWith writerOpts <$> chaoDocPandocCompiler
toc <- writePandocWith writerOpts <$> chaoDocPandocCompiler cslPath
pure $
mconcat
[ ctx,
+1
View File
@@ -3,6 +3,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="googlebot" content="noindex" />
<title>$title$</title>
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/fonts.css" />
<link rel="stylesheet" href="/css/default.css" />
<link rel="stylesheet" href="/css/pygentize.css" />
+2 -1
View File
@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="googlebot" content="noindex">
<title></title>
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/fonts.css" />
<link rel="stylesheet" href="/css/default.css" />
<link rel="stylesheet" href="/css/pygentize.css" />
@@ -31,4 +32,4 @@
</div>
</body>
</html>
</html>
+4 -4
View File
@@ -10,10 +10,10 @@ $partial("templates/head.html")$
<!-- A table of contents on the left side, but only for screens
that are big enough -->
<div id="contents-big">
<p class="mini-header"> <a href="#">Contents</a></p>
<div class="toc-scroll">
$toc$
</div>
<p class="mini-header">Notes <a id="up-arrow" href="/"></a></p>
$partial("templates/notes-list.html")$
<p class="mini-header">Contents <a id="up-arrow" href="#"></a></p>
$toc$
</div>
</div>
<div class="text-space">
+7
View File
@@ -0,0 +1,7 @@
<ul class="notes-list">
$for(notes)$
<li>
<a href="$url$">$title$</a>
</li>
$endfor$
</ul>
+1 -7
View File
@@ -1,8 +1,2 @@
<h1 class="pagetitle">$title$</h1>
<ul>
$for(posts)$
<li>
<a href="$url$">$title$</a>
</li>
$endfor$
</ul>
$partial("templates/notes-list.html")$