Compare commits
3 Commits
e419366615
...
modern_the
| Author | SHA1 | Date | |
|---|---|---|---|
| 742d06f3c7 | |||
| f226f440a5 | |||
| 6534dc58ab |
@@ -1,90 +1,130 @@
|
|||||||
.theorem-environment {
|
.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;
|
font-style: italic;
|
||||||
margin-top: 1em;
|
}
|
||||||
padding: 0.5em;
|
|
||||||
background-color: whitesmoke;
|
.theorem-environment > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theorem-environment > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header {
|
.theorem-header {
|
||||||
font-weight: bold;
|
color: var(--color-text);
|
||||||
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header .index:before {
|
.theorem-header .index:before {
|
||||||
content: ' ';
|
content: " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header .name:before {
|
.theorem-header .name:before {
|
||||||
content: ' (';
|
content: " (";
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header .name:after {
|
.theorem-header .name:after {
|
||||||
content: ')';
|
content: ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header:after {
|
.theorem-header:after {
|
||||||
content: '.\2002\2002';
|
content: ".\2002\2002";
|
||||||
}
|
}
|
||||||
|
|
||||||
.theorem-header+p {
|
.theorem-header + p {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Proof .type {
|
.Proof {
|
||||||
font-style: italic;
|
padding-right: 2.5rem;
|
||||||
font-weight: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Proof {
|
.Proof .type {
|
||||||
background: none;
|
color: var(--color-linkh);
|
||||||
font-style: normal;
|
font-weight: 400;
|
||||||
position: relative;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Proof:after {
|
.Proof:after {
|
||||||
content: '∎';
|
content: "\220e";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0px;
|
right: 1rem;
|
||||||
bottom: 0px;
|
bottom: 0.85rem;
|
||||||
|
color: var(--color-tag1);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.postindex {
|
table.postindex {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.postindex cite {
|
table.postindex cite {
|
||||||
font-style: normal;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
table.postindex td.right {
|
table.postindex td.right {
|
||||||
text-align: right;
|
|
||||||
width: 11ex;
|
width: 11ex;
|
||||||
|
color: var(--color-tag1);
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section-number {
|
.header-section-number {
|
||||||
margin-right: 10px;
|
margin-right: 0.55rem;
|
||||||
|
color: var(--color-tag1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section-number:after {
|
.header-section-number:after {
|
||||||
content: '.';
|
content: ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
.csl-entry {
|
.csl-entry {
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.csl-left-margin {
|
.csl-left-margin {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding-right: 0.5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
padding-right: 0.75em;
|
||||||
|
color: var(--color-tag1);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.csl-right-inline {
|
.csl-right-inline {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
.csl-right-inline a{
|
|
||||||
|
.csl-right-inline a {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
615
css/default.css
615
css/default.css
@@ -1,69 +1,170 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-text: black;
|
color-scheme: light;
|
||||||
--color-tag1: gray;
|
--color-text: #162031;
|
||||||
--color-tag2: darkolivegreen;
|
--color-tag1: #6c788c;
|
||||||
--color-bg: white;
|
--color-tag2: #547157;
|
||||||
--color-link: #337ab7;
|
--color-bg: #f3f6fb;
|
||||||
--color-linkhbg: #e6f0ff;
|
--color-surface: rgba(255, 255, 255, 0.84);
|
||||||
--color-linkh: #002266;
|
--color-surface-strong: #ffffff;
|
||||||
--color-bq: olivedrab;
|
--color-surface-muted: #edf3fb;
|
||||||
--color-notice: #fb4f4f;
|
--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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--color-text: white;
|
color-scheme: dark;
|
||||||
--color-bg: black;
|
--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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
scroll-behavior: smooth;
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
box-sizing: content-box;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Lato", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 140%;
|
line-height: 1.5;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background-color: var(--color-bg);
|
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;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.lang-zh {
|
body.lang-zh {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-linkhbg);
|
||||||
|
color: var(--color-linkh);
|
||||||
|
}
|
||||||
|
|
||||||
|
main,
|
||||||
|
.text-space {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
body a {
|
body a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
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-space a:hover {
|
.text-space a:hover {
|
||||||
background-color: var(--color-linkhbg);
|
background-color: var(--color-linkhbg);
|
||||||
color: var(--color-linkh);
|
color: var(--color-linkh);
|
||||||
text-decoration: none;
|
text-decoration-color: transparent;
|
||||||
}
|
border-radius: 0.2rem;
|
||||||
details {
|
|
||||||
background-color: var(--color-linkhbg);
|
|
||||||
}
|
|
||||||
summary:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*mathML*/
|
body a:focus-visible {
|
||||||
.htmlmathparagraph, mtext,math {
|
outline: 2px solid var(--color-link);
|
||||||
font-family: Lete Sans Math;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
.math-container,
|
.math-container,
|
||||||
#math-container {
|
#math-container {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: .5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.math-container.math-container-tagged {
|
.math-container.math-container-tagged {
|
||||||
@@ -72,7 +173,7 @@ summary:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: .5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.math-container.math-container-tagged .math-tag-spacer {
|
.math-container.math-container-tagged .math-tag-spacer {
|
||||||
@@ -83,7 +184,7 @@ summary:hover {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: .5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.math-container.math-container-tagged .math-tag {
|
.math-container.math-container-tagged .math-tag {
|
||||||
@@ -105,6 +206,7 @@ summary:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,21 +219,25 @@ header {
|
|||||||
font-family: "IosevkaC", sans-serif;
|
font-family: "IosevkaC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* top bar*/
|
|
||||||
.navbar {
|
.navbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navright {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navright a {
|
.navright a {
|
||||||
margin: 0 0 0 1em;
|
margin: 0 0 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Links inside the navbar */
|
|
||||||
.navbar a {
|
.navbar a {
|
||||||
text-decoration: none;
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar a:visited {
|
.navbar a:visited {
|
||||||
@@ -139,37 +245,32 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
border-bottom: solid 1px var(--color-text);
|
border-bottom: 1px solid var(--color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a {
|
nav a {
|
||||||
font-size: 1.2rem;
|
display: inline-flex;
|
||||||
/*margin-left: 0.5em;*/
|
align-items: center;
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
font-size: 1.05rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uri {
|
.uri {
|
||||||
word-wrap: break-word;
|
overflow-wrap: anywhere;
|
||||||
/* Legacy support */
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
|
||||||
/* Modern property */
|
|
||||||
word-break: break-all;
|
|
||||||
/* Break long words if necessary */
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
/* Allow wrapping */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
color: var(--color-text);
|
margin-top: 3rem;
|
||||||
font-size: 0.8rem;
|
padding-right: 0.2rem;
|
||||||
margin-top: 2em;
|
color: var(--color-tag1);
|
||||||
|
font-size: 0.82rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@@ -179,47 +280,63 @@ h4,
|
|||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagetitle {
|
.pagetitle {
|
||||||
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 100%;
|
line-height: 1;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 1em;
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
font-size: 1.44rem;
|
font-size: 1.44rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-align: left;
|
line-height: 1.1;
|
||||||
line-height: 100%;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 1em;
|
margin-top: 1.75rem;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-style: normal
|
font-style: normal;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 1em;
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-style: normal
|
font-style: normal;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin-top: 1.35rem;
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
article .header {
|
article .header {
|
||||||
|
color: var(--color-tag1);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: var(--color-tag1);
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
color: var(--color-tag2);
|
color: var(--color-tag2);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -239,202 +356,375 @@ article .header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section.body {
|
section.body {
|
||||||
margin-top: 2rem;
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.body > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ascii-art {
|
.ascii-art {
|
||||||
|
overflow-x: auto;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
line-height: normal;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 1rem 0;
|
margin: 1.5rem 0;
|
||||||
padding: 0 0 0 1.5em;
|
padding: 1rem 1.25rem;
|
||||||
border-left: 3px solid var(--color-bq);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote p {
|
blockquote p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol,
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: square;
|
margin: 0 0 1.1rem;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
li {
|
|
||||||
margin-bottom: 0.15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
ul {
|
||||||
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
border: 1px solid darkolivegreen;
|
padding: 0.65rem 0.8rem;
|
||||||
border-collapse: collapse;
|
border: 1px solid var(--color-border);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: var(--color-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) td {
|
||||||
|
background-color: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
padding: 5px;
|
gap: 0.75rem;
|
||||||
margin: auto;
|
padding: 0.9rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
max-width: 80%;
|
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 {
|
figcaption {
|
||||||
/* font: italic smaller sans-serif; */
|
padding: 0 0.25rem;
|
||||||
padding: 3px;
|
color: var(--color-tag1);
|
||||||
|
font-size: 0.92rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.caption {
|
.caption {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centerimg img {
|
.centerimg img {
|
||||||
margin: 0 auto 0 auto;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
div.highlight,
|
div.highlight {
|
||||||
pre code {
|
margin: 1.5rem 0;
|
||||||
margin: auto;
|
padding: 1rem 1.1rem;
|
||||||
padding: 10px;
|
|
||||||
overflow: auto;
|
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;
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
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-family: "IosevkaC", monospace;
|
||||||
margin: 0 auto;
|
font-size: 0.92em;
|
||||||
display: inline-block;
|
|
||||||
padding: 0px 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
font-kerning: none;
|
font-kerning: none;
|
||||||
text-rendering: optimizeSpeed;
|
text-rendering: optimizeSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.draft-notice {
|
.draft-notice {
|
||||||
|
display: table;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
color: var(--color-notice);
|
color: var(--color-notice);
|
||||||
margin: 1em auto;
|
text-align: center;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
text-align: left;
|
margin-top: 0.5rem;
|
||||||
|
color: var(--color-tag1);
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin-top: 0
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle p {
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
margin-top: 2em;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 12px;
|
gap: 1rem;
|
||||||
|
margin-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery img {
|
.gallery img {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
display: block;
|
border-radius: 1rem;
|
||||||
|
box-shadow: var(--color-shadow-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* phones -- no sidebar no sidenotes*/
|
hr {
|
||||||
@media (max-width: 768px) {
|
margin: 2rem 0;
|
||||||
body {
|
border: 0;
|
||||||
/* width: 90%; */
|
border-top: 1px solid var(--color-border-strong);
|
||||||
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 {
|
.toc {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sidebar. no sidenotes */
|
/* 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 */
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
body {
|
body {
|
||||||
max-width: 1350px;
|
display: flex;
|
||||||
display: -webkit-flex;
|
|
||||||
-webkit-flex-flow: row wrap;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
-ms-flex-flow: row wrap;
|
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
padding-right: 5%;
|
max-width: 1350px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
padding: 1.5rem 5% 3rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc {
|
.toc {
|
||||||
margin-top: 5rem;
|
display: inline-block;
|
||||||
|
width: 33%;
|
||||||
|
margin-top: 4.5rem;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
width: 33%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div#contents ul,
|
div#contents ul,
|
||||||
div#contents-big ul {
|
div#contents-big ul {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.3em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.3em;
|
||||||
padding-left: 1em;
|
margin-left: 0.1rem;
|
||||||
line-height: 1.2;
|
padding-left: 1.45rem;
|
||||||
|
line-height: 1.25;
|
||||||
list-style-type: decimal;
|
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 {
|
div#contents-big ul ul {
|
||||||
list-style-type: none;
|
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 {
|
div#contents-big li + li {
|
||||||
margin-top: 0.5em
|
margin-top: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#contents-big li::marker {
|
||||||
|
color: var(--color-tag1);
|
||||||
|
font-family: "IosevkaC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#contents-big {
|
div#contents-big {
|
||||||
font-size: 80%;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-left: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
max-width: 60%;
|
|
||||||
clear: both;
|
|
||||||
margin-right: 4em;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 5rem;
|
top: 6rem;
|
||||||
left: 100%
|
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%;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
div#contents-big .mini-header {
|
div#contents-big .mini-header {
|
||||||
font-weight: bold;
|
margin: 0 0 0.45rem;
|
||||||
margin: 0;
|
color: var(--color-tag1);
|
||||||
|
font-weight: 700;
|
||||||
font-variant: small-caps;
|
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 {
|
.text-space {
|
||||||
@@ -443,7 +733,8 @@ code {
|
|||||||
max-width: 875px;
|
max-width: 875px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* sidebar+sidenotes */
|
|
||||||
|
/* sidebar + sidenotes */
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
body {
|
body {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
@@ -452,7 +743,6 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
.no-print,
|
.no-print,
|
||||||
.no-print * {
|
.no-print * {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -460,5 +750,16 @@ code {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
details,
|
||||||
|
div#contents-big,
|
||||||
|
figure,
|
||||||
|
pre,
|
||||||
|
div.highlight,
|
||||||
|
table {
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,79 @@
|
|||||||
|
:root {
|
||||||
code.sourceCode
|
--syntax-alert: #c2410c;
|
||||||
{
|
--syntax-annotation: #0f766e;
|
||||||
background: inherit
|
--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;
|
||||||
}
|
}
|
||||||
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
|
|
||||||
code span.al { color: #CB4B16; font-weight: bold; } /* Alert */
|
@media (prefers-color-scheme: dark) {
|
||||||
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
|
:root {
|
||||||
code span.at { color: #7d9029; } /* Attribute */
|
--syntax-alert: #fdba74;
|
||||||
code span.bn { color: #D33682; } /* BaseN */
|
--syntax-annotation: #99f6e4;
|
||||||
code span.bu { } /* BuiltIn */
|
--syntax-attribute: #93c5fd;
|
||||||
code span.cf { color: #5F8700; font-weight: bold; } /* ControlFlow */
|
--syntax-number: #d8b4fe;
|
||||||
code span.ch { color: #16801a; } /* Char */
|
--syntax-control: #bef264;
|
||||||
code span.cn { color: #880000; } /* Constant */
|
--syntax-char: #86efac;
|
||||||
code span.co { color: #93A1A1; font-style: italic; } /* Comment */
|
--syntax-constant: #fda4af;
|
||||||
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
|
--syntax-comment: #94a3b8;
|
||||||
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
|
--syntax-doc: #fcd34d;
|
||||||
code span.dt { background-color: #f8edff; } /* DataType */
|
--syntax-type-bg: rgba(216, 180, 254, 0.16);
|
||||||
code span.dv { color: #D33682; } /* DecVal */
|
--syntax-error: #f87171;
|
||||||
code span.er { color: #D30102; font-weight: bold; } /* Error */
|
--syntax-function: #bfdbfe;
|
||||||
code span.ex { } /* Extension */
|
--syntax-import: #5eead4;
|
||||||
code span.fl { color: #D33682; } /* Float */
|
--syntax-preprocessor: #fdba74;
|
||||||
code span.fu { } /* Function */
|
--syntax-special: #7dd3fc;
|
||||||
code span.im { color: #D70000} /* Import */
|
--syntax-string: #86efac;
|
||||||
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
|
--syntax-variable: #c4b5fd;
|
||||||
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.sourceCode {
|
||||||
code span.sc { color: #4070a0; } /* SpecialChar */
|
background: inherit;
|
||||||
code span.ss { color: #bb6688; } /* SpecialString */
|
}
|
||||||
code span.st { color: #16801a; } /* String */
|
|
||||||
code span.va { color: #19177c; } /* Variable */
|
pre > code.sourceCode > span > a:first-child::before {
|
||||||
code span.vs { color: #4070a0; } /* VerbatimString */
|
text-decoration: underline;
|
||||||
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|||||||
@@ -19,127 +19,148 @@ body {
|
|||||||
.sidenote,
|
.sidenote,
|
||||||
.marginnote,
|
.marginnote,
|
||||||
.marginnote-left {
|
.marginnote-left {
|
||||||
|
position: relative;
|
||||||
float: right;
|
float: right;
|
||||||
clear: right;
|
clear: right;
|
||||||
margin-right: -42%;
|
|
||||||
width: 36%;
|
width: 36%;
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.45rem;
|
||||||
|
margin-right: -42%;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-size: 0.8em;
|
padding: 0.85rem 1rem;
|
||||||
line-height: 1.2;
|
background-color: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: var(--color-shadow-soft);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
position: relative;
|
font-size: 0.82em;
|
||||||
|
line-height: 1.45;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.sidenote,
|
.sidenote,
|
||||||
.marginnote,
|
.marginnote,
|
||||||
.marginnote-left {
|
.marginnote-left {
|
||||||
margin-right: -40%;
|
|
||||||
width: 33%;
|
width: 33%;
|
||||||
|
margin-right: -40%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.marginnote-left {
|
.marginnote-left {
|
||||||
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
clear: left;
|
clear: left;
|
||||||
margin-left: -32%;
|
|
||||||
width: 25%;
|
width: 25%;
|
||||||
position: relative;
|
margin-left: -32%;
|
||||||
text-align: right;
|
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
|
/* The first condition is for the case of a left-aligned layout
|
||||||
on a larger screen. It's a bit awkward, sadly :/ */
|
(on a smaller screen), and the second condition for a more centered
|
||||||
@media (max-width: 1349px) or ((min-width: 1367px) and (max-width: 1620px)) {
|
layout on a larger screen. */
|
||||||
|
@media (max-width: 1349px), (min-width: 1367px) and (max-width: 1620px) {
|
||||||
.marginnote-left {
|
.marginnote-left {
|
||||||
margin-left: -33%;
|
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
margin-left: -33%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote code {
|
.sidenote > :first-child,
|
||||||
font-size: 0.94em;
|
.marginnote > :first-child,
|
||||||
|
.marginnote-left > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For some reason, although only `overflow-x` is set in `default.css`,
|
.sidenote > :last-child,
|
||||||
block code in side and marginnotes gets a vertical (!) scrollbar no
|
.marginnote > :last-child,
|
||||||
matter what; disable that.
|
.marginnote-left > :last-child {
|
||||||
*/
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenote code {
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block code in side and margin notes gets a vertical scrollbar on some
|
||||||
|
browsers; disable that. */
|
||||||
div .marginnote pre,
|
div .marginnote pre,
|
||||||
div .sidenote pre {
|
div .sidenote pre {
|
||||||
|
margin: 0.75rem 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote-number {
|
.sidenote-number {
|
||||||
counter-increment: sidenote-counter;
|
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
|
counter-increment: sidenote-counter;
|
||||||
|
font-family: "IosevkaC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote-number:after,
|
.sidenote-number:after,
|
||||||
.sidenote:before {
|
.sidenote:before {
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
|
color: var(--color-link);
|
||||||
|
font-family: "IosevkaC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote-number:after {
|
.sidenote-number:after {
|
||||||
content: counter(sidenote-counter);
|
content: counter(sidenote-counter);
|
||||||
font-size: 0.8rem;
|
|
||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Properly position siednote number and adjust position of sidenote
|
/* Properly position sidenote number and adjust position of sidenote
|
||||||
paragraphs:
|
paragraphs:
|
||||||
https://github.com/edwardtufte/tufte-css/issues/93#issuecomment-670695382
|
https://github.com/edwardtufte/tufte-css/issues/93#issuecomment-670695382
|
||||||
*/
|
*/
|
||||||
.sidenote::before {
|
.sidenote::before {
|
||||||
content: counter(sidenote-counter) " ";
|
content: counter(sidenote-counter) " ";
|
||||||
font-size: 0.8rem;
|
|
||||||
top: -0.55rem;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: -0.55rem;
|
||||||
right: calc(100% + 0.5em);
|
right: calc(100% + 0.5em);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote p {
|
.sidenote p {
|
||||||
margin: 1em 0;
|
margin: 0.85em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenote p:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidenote p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* */
|
|
||||||
|
|
||||||
input.margin-toggle {
|
input.margin-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
label.sidenote-number {
|
label.sidenote-number {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-height: 2rem; /* should be less than or equal to paragraph line-height */
|
max-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
label.margin-toggle:not(.sidenote-number) {
|
label.margin-toggle:not(.sidenote-number) {
|
||||||
display: none;
|
display: none;
|
||||||
|
color: var(--color-link);
|
||||||
|
font-family: "IosevkaC", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iframe-wrapper {
|
.iframe-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 56.25%; /* 16:9 */
|
|
||||||
padding-top: 25px;
|
|
||||||
height: 0;
|
height: 0;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iframe-wrapper iframe {
|
.iframe-wrapper iframe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
@@ -153,23 +174,24 @@ label.margin-toggle:not(.sidenote-number) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Linkify sidenotes iff they are clickable */
|
/* Linkify sidenotes iff they are clickable. */
|
||||||
.margin-toggle,
|
.margin-toggle,
|
||||||
.sidenote-number:after {
|
.sidenote-number:after {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--color-link-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-toggle:checked + .sidenote,
|
.margin-toggle:checked + .sidenote,
|
||||||
.margin-toggle:checked + .marginnote,
|
.margin-toggle:checked + .marginnote,
|
||||||
.margin-toggle:checked + .marginnote-left {
|
.margin-toggle:checked + .marginnote-left {
|
||||||
display: block;
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
display: block;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
margin: 1rem 2.5%;
|
margin: 1rem 2.5%;
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
cabal-version: 2.4
|
name: hakyll-blog
|
||||||
name: hakysidian
|
|
||||||
version: 0.1.0.0
|
version: 0.1.0.0
|
||||||
build-type: Simple
|
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 hakysidian
|
executable site
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
main-is: site.hs
|
main-is: site.hs
|
||||||
other-modules: ChaoDoc, SideNoteHTML, Pangu, Paths_hakysidian
|
other-modules: ChaoDoc, SideNoteHTML, Pangu
|
||||||
autogen-modules: Paths_hakysidian
|
|
||||||
build-depends: base >= 4.18
|
build-depends: base >= 4.18
|
||||||
, hakyll >= 4.15
|
, hakyll >= 4.15
|
||||||
, mtl >= 2.2.2
|
, mtl >= 2.2.2
|
||||||
@@ -29,12 +20,7 @@ executable hakysidian
|
|||||||
-- , process
|
-- , process
|
||||||
-- , regex-compat
|
-- , regex-compat
|
||||||
, array
|
, array
|
||||||
, directory
|
|
||||||
, filepath
|
, filepath
|
||||||
, process
|
|
||||||
, time
|
|
||||||
, wai-app-static
|
|
||||||
, warp
|
|
||||||
-- , ghc-syntax-highlighter
|
-- , ghc-syntax-highlighter
|
||||||
-- , blaze-html >= 0.9
|
-- , blaze-html >= 0.9
|
||||||
, megaparsec
|
, megaparsec
|
||||||
13
makefile
13
makefile
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
COMMANDS := build watch rebuild clean
|
COMMANDS := build watch rebuild clean
|
||||||
BIN := hakysidian
|
|
||||||
.PHONY: $(COMMANDS), publish
|
.PHONY: $(COMMANDS), publish
|
||||||
|
|
||||||
# Set the default goal, so running 'make' without arguments will run 'make build'.
|
# Set the default goal, so running 'make' without arguments will run 'make build'.
|
||||||
@@ -8,18 +7,18 @@ BIN := hakysidian
|
|||||||
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
$(COMMANDS): $(BIN)
|
$(COMMANDS): site
|
||||||
@echo "Running command: ./$(BIN) $@"
|
@echo "Running command: ./site $@"
|
||||||
-@./$(BIN) $@
|
-@./site $@
|
||||||
|
|
||||||
|
|
||||||
# --- Rules ---
|
# --- Rules ---
|
||||||
# using relative symlinks should be fine since everything only works at ./
|
# using relative symlinks should be fine since everything only works at ./
|
||||||
|
|
||||||
|
|
||||||
$(BIN): src/site.hs src/ChaoDoc.hs
|
site: src/site.hs src/ChaoDoc.hs
|
||||||
cabal build exe:hakysidian
|
cabal build
|
||||||
ln -sf "$(shell cabal list-bin exe:hakysidian)" $(BIN)
|
ln -sf "$(shell cabal list-bin exe:site)" site
|
||||||
|
|
||||||
# move from katex to mathjax
|
# move from katex to mathjax
|
||||||
# katex_cli:
|
# katex_cli:
|
||||||
|
|||||||
@@ -124,9 +124,9 @@ preprocessTheorems (Div attr xs)
|
|||||||
attr' = addAttr attr "type" theoremType
|
attr' = addAttr attr "type" theoremType
|
||||||
preprocessTheorems x = return x
|
preprocessTheorems x = return x
|
||||||
|
|
||||||
theoremFilter :: Text -> Pandoc -> Pandoc
|
theoremFilter :: Pandoc -> Pandoc
|
||||||
theoremFilter mathMacros doc =
|
theoremFilter doc =
|
||||||
walk (makeTheorem mathMacros) $
|
walk makeTheorem $
|
||||||
autorefFilter $
|
autorefFilter $
|
||||||
evalState (walkM preprocessTheorems normalizedDoc) 1
|
evalState (walkM preprocessTheorems normalizedDoc) 1
|
||||||
where
|
where
|
||||||
@@ -171,13 +171,24 @@ autorefFilter x = walk (autoref links) x
|
|||||||
where
|
where
|
||||||
links = query theoremIndex x
|
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 :: Text -> Text -> Text
|
||||||
prependMacros macros body = macros <> "\n\n" <> body
|
prependMacros macros body = macros <> "\n\n" <> body
|
||||||
|
|
||||||
thmNamePandoc :: Text -> Text -> Pandoc
|
prependMathMacros :: Text -> Text
|
||||||
thmNamePandoc mathMacros x =
|
prependMathMacros = prependMacros mathMacros
|
||||||
|
|
||||||
|
thmNamePandoc :: Text -> Pandoc
|
||||||
|
thmNamePandoc x =
|
||||||
fromRight (Pandoc nullMeta []) . runPure $
|
fromRight (Pandoc nullMeta []) . runPure $
|
||||||
readMarkdown chaoDocRead (prependMacros mathMacros x)
|
readMarkdown chaoDocRead (prependMathMacros x)
|
||||||
|
|
||||||
obsidianTheoremFilter :: Pandoc -> Pandoc
|
obsidianTheoremFilter :: Pandoc -> Pandoc
|
||||||
obsidianTheoremFilter = attachStandaloneLabels . walk rewriteObsidianBlockQuote
|
obsidianTheoremFilter = attachStandaloneLabels . walk rewriteObsidianBlockQuote
|
||||||
@@ -379,8 +390,8 @@ unsnoc (x : xs) = do
|
|||||||
(prefix, lastElem) <- unsnoc xs
|
(prefix, lastElem) <- unsnoc xs
|
||||||
return (x : prefix, lastElem)
|
return (x : prefix, lastElem)
|
||||||
|
|
||||||
makeTheorem :: Text -> Block -> Block
|
makeTheorem :: Block -> Block
|
||||||
makeTheorem mathMacros (Div attr xs)
|
makeTheorem (Div attr xs)
|
||||||
| isNothing t = Div attr xs
|
| isNothing t = Div attr xs
|
||||||
| otherwise = Div (addClass attr "theorem-environment") (Plain [header] : xs)
|
| otherwise = Div (addClass attr "theorem-environment") (Plain [header] : xs)
|
||||||
where
|
where
|
||||||
@@ -397,23 +408,26 @@ makeTheorem mathMacros (Div attr xs)
|
|||||||
nametext =
|
nametext =
|
||||||
if isNothing name
|
if isNothing name
|
||||||
then Str ""
|
then Str ""
|
||||||
else Span (addClass nullAttr "name") (pandocToInline $ thmNamePandoc mathMacros $ fromJust name)
|
else Span (addClass nullAttr "name") (pandocToInline $ thmNamePandoc $ fromJust name)
|
||||||
makeTheorem _ x = x
|
makeTheorem x = x
|
||||||
|
|
||||||
bibFile :: T.Text
|
-- bib from https://github.com/chaoxu/chaoxu.github.io/tree/develop
|
||||||
|
cslFile :: String
|
||||||
|
cslFile = "bib_style.csl"
|
||||||
|
|
||||||
|
bibFile :: String
|
||||||
bibFile = "reference.bib"
|
bibFile = "reference.bib"
|
||||||
|
|
||||||
chaoDocPandocCompiler :: FilePath -> Compiler (Item Pandoc)
|
chaoDocPandocCompiler :: Compiler (Item Pandoc)
|
||||||
chaoDocPandocCompiler cslPath = do
|
chaoDocPandocCompiler = do
|
||||||
macros <- T.pack <$> loadBody "math-macros.md"
|
macros <- T.pack <$> loadBody "math-macros.md"
|
||||||
void (loadBody "reference.bib" :: Compiler String)
|
|
||||||
body <- getResourceBody
|
body <- getResourceBody
|
||||||
let bodyWithMacros =
|
let bodyWithMacros =
|
||||||
fmap (T.unpack . prependMacros macros . T.pack) body
|
fmap (T.unpack . prependMacros macros . T.pack) body
|
||||||
myReadPandocBiblio chaoDocRead (T.pack cslPath) bibFile (myFilter macros) bodyWithMacros
|
myReadPandocBiblio chaoDocRead (T.pack cslFile) (T.pack bibFile) myFilter bodyWithMacros
|
||||||
|
|
||||||
chaoDocCompiler :: FilePath -> Compiler (Item String)
|
chaoDocCompiler :: Compiler (Item String)
|
||||||
chaoDocCompiler cslPath = chaoDocPandocCompiler cslPath <&> writePandocWith chaoDocWrite
|
chaoDocCompiler = chaoDocPandocCompiler <&> writePandocWith chaoDocWrite
|
||||||
|
|
||||||
addMeta :: T.Text -> MetaValue -> Pandoc -> Pandoc
|
addMeta :: T.Text -> MetaValue -> Pandoc -> Pandoc
|
||||||
addMeta name value (Pandoc meta a) =
|
addMeta name value (Pandoc meta a) =
|
||||||
@@ -451,9 +465,8 @@ myReadPandocBiblio ropt csl biblio pdfilter item = do
|
|||||||
-- let a x = itemSetBody (pandoc' x)
|
-- let a x = itemSetBody (pandoc' x)
|
||||||
return $ fmap (const pandoc') item
|
return $ fmap (const pandoc') item
|
||||||
|
|
||||||
myFilter :: Text -> Pandoc -> Pandoc
|
myFilter :: Pandoc -> Pandoc
|
||||||
myFilter mathMacros =
|
myFilter = usingSideNotesHTML chaoDocWrite . theoremFilter . panguFilter . displayMathFilter
|
||||||
usingSideNotesHTML chaoDocWrite . theoremFilter mathMacros . panguFilter . displayMathFilter
|
|
||||||
|
|
||||||
-- pangu filter
|
-- pangu filter
|
||||||
lastChar :: Inline -> Maybe Char
|
lastChar :: Inline -> Maybe Char
|
||||||
|
|||||||
844
src/site.hs
844
src/site.hs
@@ -1,740 +1,230 @@
|
|||||||
{-# LANGUAGE BlockArguments #-}
|
{-# LANGUAGE BlockArguments #-}
|
||||||
{-# LANGUAGE DerivingStrategies #-}
|
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE StandaloneKindSignatures #-}
|
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
|
||||||
import ChaoDoc
|
import ChaoDoc
|
||||||
import Control.Concurrent (forkIO, threadDelay)
|
import Data.List (sortOn)
|
||||||
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 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
|
||||||
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 System.FilePath
|
||||||
import Network.Wai.Application.Static (staticApp)
|
import Text.Pandoc
|
||||||
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/
|
-- https://www.rohanjain.in/hakyll-clean-urls/
|
||||||
cleanRoute :: Routes
|
cleanRoute :: Routes
|
||||||
cleanRoute = customRoute createIndexRoute
|
cleanRoute = customRoute createIndexRoute
|
||||||
where
|
where
|
||||||
createIndexRoute ident = takeDirectory path </> takeBaseName path </> "index.html"
|
createIndexRoute ident = takeDirectory p </> takeBaseName p </> "index.html"
|
||||||
where
|
where
|
||||||
path = toFilePath ident
|
p = toFilePath ident
|
||||||
|
|
||||||
cleanIndexHtmls :: Item String -> Compiler (Item String)
|
cleanIndexHtmls :: Item String -> Compiler (Item String)
|
||||||
cleanIndexHtmls = return . fmap (replaceAll pattern replacement)
|
cleanIndexHtmls = return . fmap (replaceAll pattern replacement)
|
||||||
where
|
where
|
||||||
pattern :: String
|
pattern :: String = "/index.html"
|
||||||
pattern = "/index.html"
|
replacement :: String -> String = const "/"
|
||||||
|
|
||||||
replacement :: String -> String
|
|
||||||
replacement = const "/"
|
|
||||||
|
|
||||||
loadNoteLinks :: Compiler [Item String]
|
|
||||||
loadNoteLinks = do
|
|
||||||
noteIds <- sortOn toFilePath <$> getMatches notesPattern
|
|
||||||
pure [Item noteId "" | noteId <- noteIds]
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
siteConfiguration :: FilePath -> Configuration
|
config :: Configuration
|
||||||
siteConfiguration projectRoot =
|
config =
|
||||||
defaultConfiguration
|
defaultConfiguration
|
||||||
{ destinationDirectory = projectRoot </> "_site",
|
{ ignoreFile = \path ->
|
||||||
storeDirectory = projectRoot </> "_cache",
|
ignoreFile defaultConfiguration path
|
||||||
tmpDirectory = projectRoot </> "_cache" </> "tmp",
|
|| ".git" `elem` splitDirectories (normalise path)
|
||||||
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 :: IO ()
|
||||||
main = do
|
main = hakyllWith config $ 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 (unlines 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
|
match "images/**" $ do
|
||||||
route idRoute
|
route idRoute
|
||||||
compile copyFileCompiler
|
compile copyFileCompiler
|
||||||
|
|
||||||
match "math-macros.md" $
|
match "math-macros.md" $ compile getResourceBody
|
||||||
compile getResourceBody
|
|
||||||
|
|
||||||
match "reference.bib" $
|
match "fonts/*.woff2" $ do
|
||||||
compile getResourceBody
|
route idRoute
|
||||||
|
compile copyFileCompiler
|
||||||
|
|
||||||
mapM_ createBundledCss bundledCssFiles
|
match "favicon.ico" $ do
|
||||||
mapM_ createBundledCopy bundledFontFiles
|
route idRoute
|
||||||
createBundledCopy "favicon.ico"
|
compile copyFileCompiler
|
||||||
mapM_ createBundledTemplate bundledTemplateFiles
|
|
||||||
|
|
||||||
match notesPattern $ do
|
-- 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
|
||||||
route cleanRoute
|
route cleanRoute
|
||||||
compile $ do
|
compile $ do
|
||||||
notes <- loadNoteLinks
|
tocCtx <- getTocCtx defaultContext
|
||||||
tocCtx <- getTocCtx cslPath (listField "notes" defaultContext (pure notes) <> defaultContext)
|
chaoDocCompiler
|
||||||
chaoDocCompiler cslPath
|
|
||||||
>>= loadAndApplyTemplate "templates/note.html" tocCtx
|
>>= loadAndApplyTemplate "templates/note.html" tocCtx
|
||||||
>>= relativizeUrls
|
>>= relativizeUrls
|
||||||
|
|
||||||
create ["index.html"] $ do
|
create ["index.html"] $ do
|
||||||
route idRoute
|
route idRoute
|
||||||
compile $ do
|
compile $ do
|
||||||
notes <- loadNoteLinks
|
notes <- sortOn (toFilePath . itemIdentifier) <$> loadAll "notes/*"
|
||||||
let notesCtx =
|
let notesCtx =
|
||||||
listField "notes" defaultContext (pure notes)
|
listField "posts" defaultContext (return notes)
|
||||||
<> constField "title" "Notes"
|
`mappend` constField "title" "Notes"
|
||||||
<> defaultContext
|
`mappend` defaultContext
|
||||||
makeItem ""
|
makeItem ""
|
||||||
>>= loadAndApplyTemplate "templates/notes.html" notesCtx
|
>>= loadAndApplyTemplate "templates/notes.html" notesCtx
|
||||||
>>= loadAndApplyTemplate "templates/index.html" notesCtx
|
>>= loadAndApplyTemplate "templates/index.html" notesCtx
|
||||||
>>= relativizeUrls
|
>>= relativizeUrls
|
||||||
>>= cleanIndexHtmls
|
>>= cleanIndexHtmls
|
||||||
|
|
||||||
createBundledCss :: FilePath -> Rules ()
|
-- create ["archive.html"] $ do
|
||||||
createBundledCss relPath = create [fromFilePath relPath] $ do
|
-- route cleanRoute
|
||||||
route idRoute
|
-- compile $ do
|
||||||
compile bundledCssCompiler
|
-- 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
|
||||||
|
|
||||||
createBundledCopy :: FilePath -> Rules ()
|
-- create ["draft.html"] $ do
|
||||||
createBundledCopy relPath = create [fromFilePath relPath] $ do
|
-- route cleanRoute
|
||||||
route idRoute
|
-- compile $ do
|
||||||
compile bundledCopyCompiler
|
-- 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
|
||||||
|
|
||||||
createBundledTemplate :: FilePath -> Rules ()
|
-- match "index.html" $ do
|
||||||
createBundledTemplate relPath =
|
-- route idRoute
|
||||||
create [fromFilePath relPath] $
|
-- compile $ do
|
||||||
compile bundledTemplateCompiler
|
-- 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
|
||||||
|
|
||||||
bundledAssetPath :: Compiler FilePath
|
match "templates/*" $ compile templateBodyCompiler
|
||||||
bundledAssetPath = do
|
|
||||||
ident <- getUnderlying
|
|
||||||
unsafeCompiler $ Paths.getDataFileName (toFilePath ident)
|
|
||||||
|
|
||||||
bundledTextCompiler :: Compiler (Item String)
|
-- https://robertwpearce.com/hakyll-pt-2-generating-a-sitemap-xml-file.html
|
||||||
bundledTextCompiler = do
|
-- create ["sitemap.xml"] $ do
|
||||||
assetPath <- bundledAssetPath
|
-- route idRoute
|
||||||
contents <- unsafeCompiler (readFile assetPath)
|
-- compile $ do
|
||||||
makeItem contents
|
-- posts <- recentFirst =<< loadAll "posts/*"
|
||||||
|
-- singlePages <- loadAll (fromList ["about.md"])
|
||||||
bundledCssCompiler :: Compiler (Item String)
|
-- let pages = posts <> singlePages
|
||||||
bundledCssCompiler = fmap compressCss <$> bundledTextCompiler
|
-- sitemapCtx =
|
||||||
|
-- constField "root" root
|
||||||
bundledCopyCompiler :: Compiler (Item CopyFile)
|
-- <> listField "pages" postCtx (return pages) -- here
|
||||||
bundledCopyCompiler = do
|
-- makeItem ""
|
||||||
assetPath <- bundledAssetPath
|
-- >>= loadAndApplyTemplate "templates/sitemap.xml" sitemapCtx
|
||||||
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
|
-- toc from https://github.com/slotThe/slotThe.github.io
|
||||||
getTocCtx :: FilePath -> Context a -> Compiler (Context a)
|
getTocCtx :: Context a -> Compiler (Context a)
|
||||||
getTocCtx cslPath ctx = do
|
getTocCtx ctx = do
|
||||||
noToc <- (Just "true" ==) <$> (getUnderlying >>= (`getMetadataField` "no-toc"))
|
noToc <- (Just "true" ==) <$> (getUnderlying >>= (`getMetadataField` "no-toc"))
|
||||||
writerOpts <- mkTocWriter defaultHakyllWriterOptions
|
writerOpts <- mkTocWriter defaultHakyllWriterOptions
|
||||||
toc <- writePandocWith writerOpts <$> chaoDocPandocCompiler cslPath
|
toc <- writePandocWith writerOpts <$> chaoDocPandocCompiler
|
||||||
pure $
|
pure $
|
||||||
mconcat
|
mconcat
|
||||||
[ ctx,
|
[ ctx,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="googlebot" content="noindex" />
|
<meta name="googlebot" content="noindex" />
|
||||||
<title>$title$</title>
|
<title>$title$</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<link rel="stylesheet" href="/css/fonts.css" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="stylesheet" href="/css/default.css" />
|
<link rel="stylesheet" href="/css/default.css" />
|
||||||
<link rel="stylesheet" href="/css/pygentize.css" />
|
<link rel="stylesheet" href="/css/pygentize.css" />
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="googlebot" content="noindex">
|
<meta name="googlebot" content="noindex">
|
||||||
<title></title>
|
<title></title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<link rel="stylesheet" href="/css/fonts.css" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="stylesheet" href="/css/default.css" />
|
<link rel="stylesheet" href="/css/default.css" />
|
||||||
<link rel="stylesheet" href="/css/pygentize.css" />
|
<link rel="stylesheet" href="/css/pygentize.css" />
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ $partial("templates/head.html")$
|
|||||||
<!-- A table of contents on the left side, but only for screens
|
<!-- A table of contents on the left side, but only for screens
|
||||||
that are big enough -->
|
that are big enough -->
|
||||||
<div id="contents-big">
|
<div id="contents-big">
|
||||||
<p class="mini-header">Notes <a id="up-arrow" href="/">←</a></p>
|
<p class="mini-header"> <a href="#">Contents</a></p>
|
||||||
$partial("templates/notes-list.html")$
|
<div class="toc-scroll">
|
||||||
<p class="mini-header">Contents <a id="up-arrow" href="#">↑</a></p>
|
|
||||||
$toc$
|
$toc$
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="text-space">
|
<div class="text-space">
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<h1 class="pagetitle">$title$</h1>
|
<h1 class="pagetitle">$title$</h1>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<ul class="notes-list">
|
|
||||||
$for(notes)$
|
|
||||||
<li>
|
|
||||||
<a href="$url$">$title$</a>
|
|
||||||
</li>
|
|
||||||
$endfor$
|
|
||||||
</ul>
|
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
<h1 class="pagetitle">$title$</h1>
|
<h1 class="pagetitle">$title$</h1>
|
||||||
$partial("templates/notes-list.html")$
|
<ul>
|
||||||
|
$for(posts)$
|
||||||
|
<li>
|
||||||
|
<a href="$url$">$title$</a>
|
||||||
|
</li>
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
Reference in New Issue
Block a user