17 Commits

Author SHA1 Message Date
77c0b29b4c fix gitea actions missing go
All checks were successful
Lint / shellcheck (push) Successful in 16s
Release Standalone Builder / build (release) Successful in 1m8s
2026-03-11 08:01:26 +01:00
9ae965662c Actions! Work!
Some checks failed
Lint / shellcheck (push) Successful in 48s
Release Standalone Builder / build (release) Failing after 6s
2026-03-11 07:51:31 +01:00
9ced2af562 Actions
Some checks failed
Lint / shellcheck (push) Has been cancelled
2026-03-11 07:42:45 +01:00
f407b1c4af extra navbar links 2026-03-09 20:05:42 +01:00
bee12ce8c1 Update markdown_inline.awk 2026-03-09 10:40:30 +01:00
696dc1c142 Fix building from ./ 2026-03-08 19:28:06 +01:00
c2416e731b Turn off antialiasing in the default style 2026-03-08 17:54:10 +01:00
01fd55001a html tag fix 2026-03-08 17:44:28 +01:00
b1f69673d1 Recursive ignore files 2026-03-08 14:38:15 +01:00
6418b64672 Better logo style 2026-03-08 14:26:05 +01:00
e3cc1c1688 Some fixes, .kewtignore, .kewtpreserve and .kewthide 2026-03-08 14:18:45 +01:00
1f5d63c035 Add a button 2026-03-07 20:52:05 +01:00
6e445d5223 Fix links getting linified when they shouldn't 2026-03-07 20:15:00 +01:00
da606918a2 Remove the warning header from readme 2026-03-07 20:04:02 +01:00
7d4de6d07a Update README.md 2026-03-07 20:03:21 +01:00
f580ed9cab Move all awk functions to separate files 2026-03-07 19:42:37 +01:00
3258616282 Better sidebar sorting 2026-03-07 18:32:13 +01:00
31 changed files with 1304 additions and 774 deletions

16
.gitea/workflows/lint.yml Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on:
push:
branches: [ main, master ]
pull_request:
jobs:
shellcheck:
runs-on: local
steps:
- uses: actions/checkout@v4
- name: Install Shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run Shellcheck
run: shellcheck kewt.sh markdown.sh tools/build-standalone.sh || true

View File

@@ -0,0 +1,29 @@
name: Release Standalone Builder
on:
release:
types: [published]
jobs:
build:
runs-on: local
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build standalone executable
run: |
chmod +x tools/build-standalone.sh
./tools/build-standalone.sh
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Upload Release Asset
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt
api_key: '${{secrets.GITEA_TOKEN}}'

16
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on:
push:
branches: [ main, master ]
pull_request:
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run Shellcheck
run: shellcheck kewt.sh markdown.sh tools/build-standalone.sh || true

25
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Release Standalone Builder
on:
release:
types: [published]
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build standalone executable
run: |
chmod +x tools/build-standalone.sh
./tools/build-standalone.sh
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
files: kewt

1
.gitignore vendored
View File

@@ -1 +1,2 @@
out/ out/
kewt

View File

@@ -38,6 +38,11 @@ style = "kewt"
dir_indexes = true dir_indexes = true
single_file_index = true single_file_index = true
flatten = false flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href="https://kewt.krzak.org">kewt</a>" footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = "" logo = ""
display_logo = false display_logo = false
@@ -51,6 +56,11 @@ favicon = ""
- `dir_indexes` generate directory index pages when missing `index.md` - `dir_indexes` generate directory index pages when missing `index.md`
- `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html` - `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html`
- `flatten` flatten sidebar directory levels - `flatten` flatten sidebar directory levels
- `order` comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` text for the home link in navigation (default: "Home")
- `show_home_in_nav` show home link in navigation (default: true)
- `nav_links` comma separated extra nav links, as bare URLs or Markdown links like `[Label](https://example.com)`
- `nav_extra` raw HTML appended inside the `<nav>` after the generated link list
- `footer` footer html/text shown at the bottom of pages - `footer` footer html/text shown at the bottom of pages
- `logo` logo image path (used in header if enabled) - `logo` logo image path (used in header if enabled)
- `display_logo` show logo in header - `display_logo` show logo in header
@@ -58,6 +68,12 @@ favicon = ""
- `logo_as_favicon` use `logo` as favicon - `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set) - `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
## Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds ## Embeds
- `\![link]`: - `\![link]`:
@@ -73,6 +89,5 @@ favicon = ""
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin) - Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23) - Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
# Warning >[!WARNING]
>![WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so... >Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

14
awk/blockquote.awk Normal file
View File

@@ -0,0 +1,14 @@
BEGIN { in_bq = 0 }
/^>[[:space:]]?/ {
if (!in_bq) { print "<blockquote>"; in_bq = 1 }
sub(/^>[[:space:]]?/, "", $0)
print $0
next
}
{
if (in_bq) { print "</blockquote>"; in_bq = 0 }
print
}
END {
if (in_bq) print "</blockquote>"
}

View File

@@ -0,0 +1,46 @@
function cap(s) { return toupper(substr(s, 1, 1)) tolower(substr(s, 2)) }
BEGIN { count = 0 }
{ lines[++count] = $0 }
END {
i = 1
while (i <= count) {
if (lines[i] == "<blockquote>") {
j = i + 1
while (j <= count && lines[j] != "</blockquote>") j++
if (j <= count) {
first = ""
first_idx = 0
for (k = i + 1; k < j; k++) {
if (lines[k] != "") {
first = lines[k]
first_idx = k
break
}
}
if (first ~ /^\[![A-Za-z]+\]$/) {
kind = first
sub(/^\[!/, "", kind)
sub(/\]$/, "", kind)
lkind = tolower(kind)
if (lkind == "note" || lkind == "tip" || lkind == "important" || lkind == "warning" || lkind == "caution") {
print "<div class=\"admonition admonition-" lkind "\">"
print "<p class=\"admonition-title\">" cap(lkind) "</p>"
has_body = 0
for (k = first_idx + 1; k < j; k++) {
if (lines[k] != "") {
print "<p>" lines[k] "</p>"
has_body = 1
}
}
if (!has_body) print "<p></p>"
print "</div>"
i = j + 1
continue
}
}
}
}
print lines[i]
i++
}
}

5
awk/breaks.awk Normal file
View File

@@ -0,0 +1,5 @@
{
if ($0 == "" && prev == "") print "<br />"
else print $0
prev = $0
}

28
awk/fenced_code.awk Normal file
View File

@@ -0,0 +1,28 @@
BEGIN { in_fence = 0; first_line = 0 }
{
if (!in_fence && $0 ~ /^```/) {
in_fence = 1
first_line = 1
next
}
if (in_fence && $0 ~ /^```[[:space:]]*$/) {
print "</code></pre>"
in_fence = 0
next
}
if (in_fence) {
gsub(/&/, "\\&amp;"); gsub(/</, "\\&lt;"); gsub(/>/, "\\&gt;")
if (first_line) {
first_line = 0
if ($0 == "") next
print "<pre><code>" $0
} else {
print
}
} else {
print
}
}
END {
if (in_fence) print "</code></pre>"
}

View File

@@ -4,6 +4,34 @@ function title_from_name(name) {
return name return name
} }
function compare_paths(p1, p2, parts1, parts2, n1, n2, i, name1, name2, lname1, lname2, w1, w2) {
n1 = split(p1, parts1, "/")
n2 = split(p2, parts2, "/")
for (i = 1; i <= n1 && i <= n2; i++) {
name1 = parts1[i]
name2 = parts2[i]
if (i == n1) gsub(/\.md$/, "", name1)
if (i == n2) gsub(/\.md$/, "", name2)
lname1 = tolower(name1)
lname2 = tolower(name2)
if (lname1 == "index" && i == n1 && lname2 != "index") return -1
if (lname2 == "index" && i == n2 && lname1 != "index") return 1
w1 = (lname1 in custom_order ? custom_order[lname1] : 999999)
w2 = (lname2 in custom_order ? custom_order[lname2] : 999999)
if (w1 < w2) return -1
if (w1 > w2) return 1
if (lname1 < lname2) return -1
if (lname1 > lname2) return 1
}
if (n1 < n2) return -1
if (n1 > n2) return 1
return 0
}
BEGIN { BEGIN {
n_dlines = split(dinfo, dlines, "\n") n_dlines = split(dinfo, dlines, "\n")
for (i = 1; i <= n_dlines; i++) { for (i = 1; i <= n_dlines; i++) {
@@ -12,6 +40,16 @@ BEGIN {
d_dirs[dparts[1]] = dparts[3] d_dirs[dparts[1]] = dparts[3]
} }
} }
n_order = split(order, oparts, ",")
for (i = 1; i <= n_order; i++) {
name = oparts[i]
sub(/^[[:space:]]*/, "", name)
sub(/[[:space:]]*$/, "", name)
if (name != "") {
custom_order[tolower(name)] = i
}
}
} }
{ {
@@ -31,9 +69,20 @@ BEGIN {
} }
END { END {
for (i = 0; i < count - 1; i++) {
for (j = 0; j < count - i - 1; j++) {
if (compare_paths(ordered_paths[j], ordered_paths[j+1]) > 0) {
tmp = ordered_paths[j]
ordered_paths[j] = ordered_paths[j+1]
ordered_paths[j+1] = tmp
}
}
}
print "<ul>" print "<ul>"
if ("index.md" in all_paths) { if (show_home_in_nav == "true" && "index.md" in all_paths) {
print "<li><a href=\"/index.html\">Home</a></li>" if (home_name == "") home_name = "Home"
print "<li><a href=\"/index.html\">" home_name "</a></li>"
} }
depth = 0 depth = 0

79
awk/headers.awk Normal file
View File

@@ -0,0 +1,79 @@
function strip_markdown(s) {
gsub(/<[^>]+>/, "", s)
gsub(/[*_`~]/, "", s)
gsub(/[\[\]]/, "", s)
gsub(/\([^\)]*\)/, "", s)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
gsub(/[[:space:]]+/, "-", s)
return s
}
function print_header(line) {
if (line ~ /^# /) {
sub(/^# /, "", line); print "<h1 id=\"" strip_markdown(line) "\">" line "</h1>"
} else if (line ~ /^## /) {
sub(/^## /, "", line); print "<h2 id=\"" strip_markdown(line) "\">" line "</h2>"
} else if (line ~ /^### /) {
sub(/^### /, "", line); print "<h3 id=\"" strip_markdown(line) "\">" line "</h3>"
} else if (line ~ /^#### /) {
sub(/^#### /, "", line); print "<h4 id=\"" strip_markdown(line) "\">" line "</h4>"
} else if (line ~ /^##### /) {
sub(/^##### /, "", line); print "<h5 id=\"" strip_markdown(line) "\">" line "</h5>"
} else if (line ~ /^###### /) {
sub(/^###### /, "", line); print "<h6 id=\"" strip_markdown(line) "\">" line "</h6>"
} else {
print line
}
}
BEGIN {
has_prev = 0
in_pre = 0
}
{
if ($0 ~ /^<pre><code>/) {
in_pre = 1
if (has_prev && prev != "") { print_header(prev); has_prev = 0 }
print
next
}
if (in_pre) {
if ($0 ~ /<\/code><\/pre>/) in_pre = 0
print
next
}
if ($0 ~ /^=+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h1 id=\"" strip_markdown(prev) "\">" prev "</h1>"
has_prev = 0
} else {
if (has_prev) print_header(prev)
print $0
has_prev = 0
}
} else if ($0 ~ /^-+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h2 id=\"" strip_markdown(prev) "\">" prev "</h2>"
has_prev = 0
} else {
if (has_prev) print_header(prev)
if (length($0) >= 3) print "<hr />"
else print $0
has_prev = 0
}
} else if ($0 ~ /^[*_]+$/ && length($0) >= 3) {
if (has_prev) print_header(prev)
print "<hr />"
has_prev = 0
} else {
if (has_prev) {
print_header(prev)
}
prev = $0
has_prev = 1
}
}
END {
if (has_prev) {
print_header(prev)
}
}

9
awk/indented_code.awk Normal file
View File

@@ -0,0 +1,9 @@
BEGIN { in_code = 0 }
/^ | / {
if (!in_code) { print "<pre><code>"; in_code = 1 }
sub(/^ | /, "", $0)
gsub(/&/, "\\&amp;"); gsub(/</, "\\&lt;"); gsub(/>/, "\\&gt;")
print; next
}
{ if (in_code) { print "</code></pre>"; in_code = 0 } print }
END { if (in_code) print "</code></pre>" }

66
awk/lists.awk Normal file
View File

@@ -0,0 +1,66 @@
BEGIN {
depth = 0
in_pre = 0
}
{
if ($0 ~ /^<pre>/) in_pre = 1
if (in_pre) {
while (depth > 0) { print "</" cur_type[depth] ">"; depth-- }
print
if ($0 ~ /<\/pre>/) in_pre = 0
next
}
line = $0
type = ""
# match list marker and its preceding spaces
if (line ~ /^[ \t]*[*+-] /) {
type = "ul"
match(line, /^[ \t]*[*+-] /)
marker_len = RLENGTH
} else if (line ~ /^[ \t]*[0-9]+\. /) {
type = "ol"
match(line, /^[ \t]*[0-9]+\. /)
marker_len = RLENGTH
}
if (type != "") {
content = substr(line, marker_len + 1)
# get indentation level
match(line, /^[ \t]*/)
indent = RLENGTH
if (depth == 0 || indent > cur_indent[depth]) {
depth++
cur_indent[depth] = indent
cur_type[depth] = type
print "<" type ">"
} else {
while (depth > 1 && indent < cur_indent[depth]) {
print "</" cur_type[depth] ">"
depth--
}
if (type != cur_type[depth]) {
print "</" cur_type[depth] ">"
cur_type[depth] = type
print "<" type ">"
}
}
print "<li>" content "</li>"
} else {
while (depth > 0) {
print "</" cur_type[depth] ">"
depth--
}
print line
}
}
END {
while (depth > 0) {
print "</" cur_type[depth] ">"
depth--
}
}

View File

@@ -119,11 +119,35 @@ function css_highlight_line(line, m, prop, val) {
return "<span class=\"tok-punc\">}</span>" return "<span class=\"tok-punc\">}</span>"
} }
if (match(line, /^([[:space:]]*)(--?[A-Za-z0-9_-]+)([[:space:]]*:[[:space:]]*)([^;]*)(;?[[:space:]]*)$/, m)) { if (line ~ /^[[:space:]]*--?[A-Za-z0-9_-]+[[:space:]]*:[[:space:]]*[^;]*;?[[:space:]]*$/) {
prop = "<span class=\"tok-prop\">" m[2] "</span>" match(line, /:[[:space:]]*/)
gsub(/var\(--[A-Za-z0-9_-]+\)/, "<span class=\"tok-var\">&</span>", m[4]) sep_pos = RSTART
val = "<span class=\"tok-val\">" m[4] "</span>" sep_len = RLENGTH
return m[1] prop m[3] val m[5]
pre_sep = substr(line, 1, sep_pos - 1)
sep = substr(line, sep_pos, sep_len)
post_sep = substr(line, sep_pos + sep_len)
match(pre_sep, /--?[A-Za-z0-9_-]+/)
prop_pos = RSTART
prop_len = RLENGTH
indent = substr(pre_sep, 1, prop_pos - 1)
prop_name = substr(pre_sep, prop_pos, prop_len)
if (match(post_sep, /;[[:space:]]*$/)) {
val_part = substr(post_sep, 1, RSTART - 1)
suffix = substr(post_sep, RSTART)
} else {
val_part = post_sep
suffix = ""
}
prop = "<span class=\"tok-prop\">" prop_name "</span>"
gsub(/var\(--[A-Za-z0-9_-]+\)/, "<span class=\"tok-var\">&</span>", val_part)
val = "<span class=\"tok-val\">" val_part "</span>"
return indent prop sep val suffix
} }
return line return line

202
awk/markdown_inline.awk Normal file
View File

@@ -0,0 +1,202 @@
BEGIN {
in_pre = 0
}
function mask_html_tags(s, out, rest, start, len, tag, token) {
out = ""
rest = s
html_tag_count = 0
while (match(rest, /<[^>]+>/)) {
out = out substr(rest, 1, RSTART - 1)
start = RSTART
len = RLENGTH
tag = substr(rest, start, len)
html_tag_count++
html_tag_token[html_tag_count] = "\034HT" html_tag_count "\034"
html_tag_value[html_tag_count] = tag
out = out html_tag_token[html_tag_count]
rest = substr(rest, start + len)
}
return out rest
}
function restore_html_tags(s, i) {
for (i = 1; i <= html_tag_count; i++) {
gsub(html_tag_token[i], html_tag_value[i], s)
}
return s
}
{
if ($0 ~ /<pre>/) {
in_pre = 1
}
if (in_pre) {
print
if ($0 ~ /<\/pre>/) {
in_pre = 0
}
next
}
line = $0
# automatic links
while (match(line, /<https?:\/\/[^>]+>/)) {
start = RSTART; len = RLENGTH
url = substr(line, start + 1, len - 2)
repl = "<a href=\"" url "\">" url "</a>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# automatic email address links
while (match(line, /<[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}>/)) {
start = RSTART; len = RLENGTH
email = substr(line, start + 1, len - 2)
repl = "<a href=\"mailto:" email "\">" email "</a>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# force-inline image syntax (double bang)
while (match(line, /!!\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /"[^"]*"/); title = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); inner = substr(token, RSTART + 1, RLENGTH - 1)
sub(/[[:space:]]*"[^"]*"/, "", inner); src = inner
repl = "<img data-force-inline=\"1\" alt=\"" alt "\" src=\"" src "\" title=\"" title "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
while (match(line, /!!\[[^\]]*\]\([^\)]+\)/)) {
start = RSTART; len = RLENGTH
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); src = substr(token, RSTART + 1, RLENGTH - 1)
repl = "<img data-force-inline=\"1\" alt=\"" alt "\" src=\"" src "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# inline image
while (match(line, /!\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH
if (start > 1 && substr(line, start - 1, 1) == "\\") break
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /"[^"]*"/); title = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); inner = substr(token, RSTART + 1, RLENGTH - 1)
sub(/[[:space:]]*"[^"]*"/, "", inner); src = inner
repl = "<img alt=\"" alt "\" src=\"" src "\" title=\"" title "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
while (match(line, /!\[[^\]]*\]\([^\)]+\)/)) {
start = RSTART; len = RLENGTH
if (start > 1 && substr(line, start - 1, 1) == "\\") break
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); src = substr(token, RSTART + 1, RLENGTH - 1)
repl = "<img alt=\"" alt "\" src=\"" src "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# inline link
while (match(line, /\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH
if (start > 1 && (substr(line, start - 1, 1) == "\\" || substr(line, start - 1, 1) == "!")) break
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); text = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /"[^"]*"/); title = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); inner = substr(token, RSTART + 1, RLENGTH - 1)
sub(/[[:space:]]*"[^"]*"/, "", inner); href = inner
repl = "<a href=\"" href "\" title=\"" title "\">" text "</a>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
while (match(line, /\[[^\]]*\]\([^\)]+\)/)) {
start = RSTART; len = RLENGTH
if (start > 1 && (substr(line, start - 1, 1) == "\\" || substr(line, start - 1, 1) == "!")) break
token = substr(line, start, len)
match(token, /\[[^\]]*\]/); text = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); href = substr(token, RSTART + 1, RLENGTH - 1)
repl = "<a href=\"" href "\">" text "</a>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# MFM font syntax
while (match(line, /\$\[font\.serif [^\]]+\]/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 13, len - 14)
line = substr(line, 1, start - 1) "<span style=\"font-family: serif;\">" content "</span>" substr(line, start + len)
}
while (match(line, /\$\[font\.monospace [^\]]+\]/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 17, len - 18)
line = substr(line, 1, start - 1) "<span style=\"font-family: monospace;\">" content "</span>" substr(line, start + len)
}
while (match(line, /\$\[font\.sans [^\]]+\]/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 12, len - 13)
line = substr(line, 1, start - 1) "<span style=\"font-family: sans-serif;\">" content "</span>" substr(line, start + len)
}
line = mask_html_tags(line)
# Bold, Italic, Strikethrough (BRE-like logic in AWK)
# Strong Bold **
while (match(line, /\*\*[^*]+\*\*/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 2, len - 4)
repl = "<strong>" content "</strong>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# Strong Bold __
while (match(line, /__[^_]+__/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 2, len - 4)
repl = "<strong>" content "</strong>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# Italic *
while (match(line, /\*[^*]+\*/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 1, len - 2)
repl = "<em>" content "</em>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# Italic _
while (match(line, /_[^_]+_/)) {
start = RSTART; len = RLENGTH
if (start > 1 && substr(line, start - 1, 1) == "\\") break
content = substr(line, start + 1, len - 2)
repl = "<em>" content "</em>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# Strikethrough ~~
while (match(line, /~~[^~]+~~/)) {
start = RSTART; len = RLENGTH
content = substr(line, start + 2, len - 4)
repl = "<strike>" content "</strike>"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
line = restore_html_tags(line)
# special characters
if (line !~ /&[A-Za-z0-9#]+;/) {
gsub(/&/, "&amp;", line)
}
p = 1
while (match(substr(line, p), /</)) {
start = p + RSTART - 1
next_char = substr(line, start + 1, 1)
if (next_char !~ /^[\/A-Za-z]/) {
line = substr(line, 1, start - 1) "&lt;" substr(line, start + 1)
p = start + 4
} else {
p = start + 1
}
}
print line
}

85
awk/mask_inline_code.awk Normal file
View File

@@ -0,0 +1,85 @@
function mask(s, t) {
t = s
gsub(/\*/, "\034P0\034", t)
gsub(/_/, "\034P1\034", t)
gsub(/`/, "\034P2\034", t)
gsub(/\[/, "\034P3\034", t)
gsub(/\]/, "\034P4\034", t)
gsub(/\(/, "\034P5\034", t)
gsub(/\)/, "\034P6\034", t)
gsub(/!/, "\034P7\034", t)
gsub(/\$/, "\034P8\034", t)
gsub(/#/, "\034P9\034", t)
gsub(/\+/, "\034P10\034", t)
gsub(/-/, "\034P11\034", t)
gsub(/\\/, "\034P12\034", t)
gsub(/</, "\034P13\034", t)
gsub(/>/, "\034P14\034", t)
return t
}
{
# backslash escapes
gsub(/\\\*/, "\034P0\034")
gsub(/\\_/, "\034P1\034")
gsub(/\\`/, "\034P2\034")
gsub(/\\\[/, "\034P3\034")
gsub(/\\\]/, "\034P4\034")
gsub(/\\\(/, "\034P5\034")
gsub(/\\\)/, "\034P6\034")
gsub(/\\!/, "\034P7\034")
gsub(/\\\$/, "\034P8\034")
gsub(/\\#/, "\034P9\034")
gsub(/\\\+/, "\034P10\034")
gsub(/\\\-/, "\034P11\034")
gsub(/\\\\/, "\034P12\034")
gsub(/\\</, "\034P13\034")
gsub(/\\>/, "\034P14\034")
# inline code (1 or 2 backticks)
line = $0
if (line ~ /^```/) {
print line
next
}
out = ""
p = 1
while (match(substr(line, p), /`+/)) {
pstart = p + RSTART - 1
plen = RLENGTH
if (plen >= 3) {
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found 1 or 2 backticks at pstart
# Search for closing marker
marker = substr(line, pstart, plen)
tail = substr(line, pstart + plen)
mpos = index(tail, marker)
if (mpos > 0) {
# Check if it is followed by more backticks
if (substr(tail, mpos + plen, 1) == "`") {
# Not a match, treat as literal
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found match!
content = substr(tail, 1, mpos - 1)
out = out substr(line, p, pstart - p)
if (plen == 2 && substr(content, 1, 1) == " " && substr(content, length(content), 1) == " ") {
content = substr(content, 2, length(content) - 2)
}
out = out "<code>" mask(content) "</code>"
p = pstart + plen + mpos + plen - 1
} else {
# No closing marker, treat as literal
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
}
}
out = out substr(line, p)
print out
}

52
awk/mask_plain.awk Normal file
View File

@@ -0,0 +1,52 @@
function find_unescaped_tag(s, tag, p, off, pos) {
p = 1
while (1) {
off = index(substr(s, p), tag)
if (off == 0) return 0
pos = p + off - 1
if (pos == 1 || substr(s, pos - 1, 1) != "\\") return pos
p = pos + 1
}
}
function mask_plain(s, t) {
t = s
gsub(/\*/, "\034P0\034", t)
gsub(/_/, "\034P1\034", t)
gsub(/`/, "\034P2\034", t)
gsub(/\[/, "\034P3\034", t)
gsub(/\]/, "\034P4\034", t)
gsub(/\(/, "\034P5\034", t)
gsub(/\)/, "\034P6\034", t)
gsub(/!/, "\034P7\034", t)
gsub(/\$/, "\034P8\034", t)
return t
}
BEGIN { in_plain = 0 }
{
line = $0
out = ""
while (1) {
if (!in_plain) {
pos = find_unescaped_tag(line, "<plain>")
if (pos == 0) {
out = out line
break
}
out = out substr(line, 1, pos - 1) "<mfmplain>"
line = substr(line, pos + 7)
in_plain = 1
} else {
pos = find_unescaped_tag(line, "</plain>")
if (pos == 0) {
out = out mask_plain(line)
line = ""
break
}
out = out mask_plain(substr(line, 1, pos - 1)) "</mfmplain>"
line = substr(line, pos + 8)
in_plain = 0
}
}
print out
}

43
awk/paragraphs.awk Normal file
View File

@@ -0,0 +1,43 @@
BEGIN {
in_p = 0
in_pre = 0
}
{
if ($0 ~ /^<pre>/) in_pre = 1
if (in_pre) {
if (in_p) { print "</p>"; in_p = 0 }
print
if ($0 ~ /<\/pre>/) in_pre = 0
next
}
if ($0 ~ /^<\/?(div|table|p|[ou]l|h[1-6]|[bh]r|blockquote|li|hr)/) {
if (in_p) {
print "</p>"
in_p = 0
}
print
next
}
if ($0 == "") {
if (in_p) {
print "</p>"
in_p = 0
}
print
next
}
if (!in_p) {
print "<p>"
in_p = 1
}
print
}
END {
if (in_p) print "</p>"
}

143
awk/pipe_tables.awk Normal file
View File

@@ -0,0 +1,143 @@
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function is_table_row(line, t) {
t = line
return (t ~ /^[[:space:]]*\|/ && t ~ /\|[[:space:]]*$/)
}
function is_table_sep(line, t) {
if (!is_table_row(line)) return 0
t = line
gsub(/[|:\-[:space:]]/, "", t)
return (t == "" && line ~ /-/)
}
function split_row(line, out, n, i, raw) {
raw = line
sub(/^[[:space:]]*\|/, "", raw)
sub(/\|[[:space:]]*$/, "", raw)
n = split(raw, out, /\|/)
for (i = 1; i <= n; i++) out[i] = trim(out[i])
return n
}
function align_for(sep, t) {
t = trim(sep)
if (t ~ /^:-+:$/) return "center"
if (t ~ /^:-+$/) return "left"
if (t ~ /^-+:$/) return "right"
return ""
}
function render_cell(cell, inner) {
inner = trim(cell)
if (inner ~ /^```.*```$/) {
sub(/^```[[:space:]]*/, "", inner)
sub(/[[:space:]]*```$/, "", inner)
return "<pre><code>" inner "</code></pre>"
}
return inner
}
BEGIN { count = 0 }
{ lines[++count] = $0 }
END {
in_pre = 0
i = 1
while (i <= count) {
if (lines[i] ~ /^<pre><code>/) {
in_pre = 1
print lines[i]
i++
continue
}
if (in_pre) {
print lines[i]
if (lines[i] ~ /^<\/code><\/pre>/) in_pre = 0
i++
continue
}
if (i < count && is_table_row(lines[i]) && is_table_sep(lines[i + 1])) {
n_header = split_row(lines[i], header)
n_sep = split_row(lines[i + 1], sep)
n_cols = (n_header > n_sep ? n_header : n_sep)
print "<table>"
print "<thead>"
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_header ? render_cell(header[c]) : "")
a = (c <= n_sep ? align_for(sep[c]) : "")
if (a != "") print "<th style=\"text-align: " a ";\">" cell "</th>"
else print "<th>" cell "</th>"
}
print "</tr>"
print "</thead>"
j = i + 2
print "<tbody>"
while (j <= count && is_table_row(lines[j])) {
n_body = split_row(lines[j], body)
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_body ? render_cell(body[c]) : "")
a = (c <= n_sep ? align_for(sep[c]) : "")
if (a != "") print "<td style=\"text-align: " a ";\">" cell "</td>"
else print "<td>" cell "</td>"
}
print "</tr>"
j++
}
print "</tbody>"
print "</table>"
i = j
continue
}
if (is_table_sep(lines[i]) && i < count && is_table_row(lines[i + 1])) {
n_sep = split_row(lines[i], sep)
n_cols = n_sep
print "<table>"
print "<thead>"
print "<tr>"
for (c = 1; c <= n_cols; c++) {
a = align_for(sep[c])
if (a != "") print "<th style=\"text-align: " a ";\"></th>"
else print "<th></th>"
}
print "</tr>"
print "</thead>"
j = i + 1
print "<tbody>"
while (j <= count && is_table_row(lines[j])) {
n_body = split_row(lines[j], body)
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_body ? render_cell(body[c]) : "")
a = (c <= n_sep ? align_for(sep[c]) : "")
if (a != "") print "<td style=\"text-align: " a ";\">" cell "</td>"
else print "<td>" cell "</td>"
}
print "</tr>"
j++
}
print "</tbody>"
print "</table>"
i = j
continue
}
print lines[i]
i++
}
}

10
awk/update_site_conf.awk Normal file
View File

@@ -0,0 +1,10 @@
BEGIN { done = 0 }
/^title[[:space:]]*=/ {
print "title = \"" new_title "\""
done = 1
next
}
{ print }
END {
if (!done) print "title = \"" new_title "\""
}

BIN
button.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

244
kewt.sh
View File

@@ -20,7 +20,7 @@ Options:
EOF EOF
} }
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk" awk_dir="$script_dir/awk"
ensure_root_defaults() { ensure_root_defaults() {
@@ -31,6 +31,11 @@ style = "kewt"
dir_indexes = true dir_indexes = true
single_file_index = true single_file_index = true
flatten = false flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href="https://kewt.krzak.org">kewt</a>" footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = "" logo = ""
display_logo = false display_logo = false
@@ -81,28 +86,14 @@ create_new_site() {
printf "# _kewt_ website\n" > "$new_dir/index.md" printf "# _kewt_ website\n" > "$new_dir/index.md"
if [ -n "$new_title" ]; then if [ -n "$new_title" ]; then
awk -v new_title="$new_title" ' awk -v new_title="$new_title" -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
BEGIN { done = 0 }
/^title[[:space:]]*=/ {
print "title = \"" new_title "\""
done = 1
next
}
{ print }
END {
if (!done) print "title = \"" new_title "\""
}
' "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
fi fi
echo "Created new site at '$new_dir'." echo "Created new site at '$new_dir'."
exit 0 exit 0
} }
generate_nav() {
dinfo=$(find "$1" -not -path '*/.*' | sort -r | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
find "$1" -name "*.md" | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
}
src="" src=""
out="" out=""
@@ -139,9 +130,9 @@ while [ $# -gt 0 ]; do
*) *)
positional_count=$((positional_count + 1)) positional_count=$((positional_count + 1))
if [ "$positional_count" -eq 1 ]; then if [ "$positional_count" -eq 1 ]; then
[ -z "$src" ] && src="$1" || die "Source already set (use either positional or --from)." if [ -z "$src" ]; then src="$1"; else die "Source already set (use either positional or --from)."; fi
elif [ "$positional_count" -eq 2 ]; then elif [ "$positional_count" -eq 2 ]; then
[ -z "$out" ] && out="$1" || die "Output already set (use either positional or --to)." if [ -z "$out" ]; then out="$1"; else die "Output already set (use either positional or --to)."; fi
else else
die "Too many positional arguments." die "Too many positional arguments."
fi fi
@@ -157,14 +148,118 @@ ensure_root_defaults
[ -z "$src" ] && src="site" [ -z "$src" ] && src="site"
[ -z "$out" ] && out="out" [ -z "$out" ] && out="out"
src="${src%/}"
out="${out%/}"
[ -d "$src" ] || die "Source directory '$src' does not exist." [ -d "$src" ] || die "Source directory '$src' does not exist."
IGNORE_ARGS="-name '.kewtignore' -o -path '$src/.*'"
if [ -f "$src/.kewtignore" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
IGNORE_ARGS="$IGNORE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
IGNORE_ARGS="$IGNORE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewtignore"
fi
find "$src" -name .kewtignore > "/tmp/kewt_ignore_$$"
while read -r ki; do
d="${ki%/.kewtignore}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
IGNORE_ARGS="$IGNORE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_ignore_$$"
rm -f "/tmp/kewt_ignore_$$"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'"
if [ -f "$src/.kewthide" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
HIDE_ARGS="$HIDE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
HIDE_ARGS="$HIDE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewthide"
fi
find "$src" -name .kewthide > "/tmp/kewt_hide_$$"
while read -r kh; do
d="${kh%/.kewthide}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_hide_$$"
rm -f "/tmp/kewt_hide_$$"
PRESERVE_ARGS="-false"
if [ -f "$src/.kewtpreserve" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
PRESERVE_ARGS="$PRESERVE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewtpreserve"
fi
find "$src" -name .kewtpreserve > "/tmp/kewt_preserve_$$"
while read -r kp; do
d="${kp%/.kewtpreserve}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_preserve_$$"
rm -f "/tmp/kewt_preserve_$$"
generate_nav() {
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v home_name="$home_name" -v show_home_in_nav="$show_home_in_nav" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
}
title="kewt" title="kewt"
style="kewt" style="kewt"
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>" footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
dir_indexes="true" dir_indexes="true"
single_file_index="true" single_file_index="true"
flatten="false" flatten="false"
order=""
home_name="Home"
show_home_in_nav="true"
nav_links=""
nav_extra=""
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo="" logo=""
display_logo="false" display_logo="false"
display_title="true" display_title="true"
@@ -196,6 +291,11 @@ load_config() {
dir_indexes) dir_indexes="$val" ;; dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;; single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;; flatten) flatten="$val" ;;
order) order="$val" ;;
home_name) home_name="$val" ;;
show_home_in_nav) show_home_in_nav="$val" ;;
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;; footer) footer="$val" ;;
logo) logo="$val" ;; logo) logo="$val" ;;
display_logo) display_logo="$val" ;; display_logo) display_logo="$val" ;;
@@ -209,6 +309,65 @@ load_config() {
load_config "./site.conf" load_config "./site.conf"
load_config "$src/site.conf" load_config "$src/site.conf"
escape_html_text() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
escape_html_attr() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/"/\&quot;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
nav_links_html() {
[ -n "$nav_links" ] || return
old_ifs=$IFS
set -f
IFS=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
[ $# -gt 0 ] || return
printf '<ul class="nav-extra-links">\n'
for raw_link in "$@"; do
link=$(printf '%s' "$raw_link" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$link" ] || continue
case "$link" in
\[*\]\(*\))
label=${link#\[}
label=${label%%\]*}
link_url=${link#*](}
link_url=${link_url%)}
;;
*)
link_url=$link
label=$(printf '%s' "$link" | sed \
-e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' \
-e 's|/$||')
[ -n "$label" ] || label="$link"
;;
esac
[ -n "$link_url" ] || continue
[ -n "$label" ] || label="$link_url"
link_attr=$(escape_html_attr "$link_url")
label_text=$(escape_html_text "$label")
printf '<li><a href="%s">%s</a></li>\n' "$link_attr" "$label_text"
done
printf '</ul>'
}
template="$src/template.html" template="$src/template.html"
[ -f "$template" ] || template="./template.html" [ -f "$template" ] || template="./template.html"
[ -f "$template" ] || die "Template '$template' not found." [ -f "$template" ] || die "Template '$template' not found."
@@ -217,6 +376,15 @@ template="$src/template.html"
mkdir -p "$out" mkdir -p "$out"
nav=$(generate_nav "$src") nav=$(generate_nav "$src")
extra_links=$(nav_links_html)
if [ -n "$extra_links" ]; then
nav="$nav
$extra_links"
fi
if [ -n "$nav_extra" ]; then
nav="$nav
$nav_extra"
fi
find_closest() { find_closest() {
target="$1" target="$1"
@@ -248,7 +416,7 @@ render_markdown() {
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")") closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")") [ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#$src/}" style_rel_to_src="${closest_style_src#"$src"/}"
case "$closest_style_src" in case "$closest_style_src" in
"$src/styles.css") style_rel_to_src="styles.css" ;; "$src/styles.css") style_rel_to_src="styles.css" ;;
"$src/style.css") style_rel_to_src="style.css" ;; "$src/style.css") style_rel_to_src="style.css" ;;
@@ -290,14 +458,15 @@ render_markdown() {
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
fi fi
MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
} }
echo "Building site from '$src' to '$out'..." echo "Building site from '$src' to '$out'..."
find "$src" -type d | sort | while read -r dir; do eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
rel_dir="${dir#$src/}" rel_dir="${dir#"$src"}"
[ "$dir" = "$src" ] && rel_dir="." rel_dir="${rel_dir#/}"
[ -z "$rel_dir" ] && rel_dir="."
out_dir="$out/$rel_dir" out_dir="$out/$rel_dir"
mkdir -p "$out_dir" mkdir -p "$out_dir"
@@ -311,9 +480,9 @@ find "$src" -type d | sort | while read -r dir; do
if [ ! -f "$dir/index.md" ]; then if [ ! -f "$dir/index.md" ]; then
if [ "$single_file_index" = "true" ]; then if [ "$single_file_index" = "true" ]; then
md_count=$(find "$dir" -maxdepth 1 -name "*.md" | wc -l) md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" -maxdepth 1 -name "*.md") md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
render_markdown "$md_file" > "$out_dir/index.html" render_markdown "$md_file" > "$out_dir/index.html"
continue continue
fi fi
@@ -324,7 +493,7 @@ find "$src" -type d | sort | while read -r dir; do
[ -z "$display_dir" ] && display_dir="/" [ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index" echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index" echo "" >> "$temp_index"
find "$dir" -maxdepth 1 -not -path '*/.*' -not -path "$dir" | sort | while read -r entry; do find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | sort | while read -r entry; do
name="${entry##*/}" name="${entry##*/}"
case "$name" in case "$name" in
template.html|site.conf|style.css|index.md) continue ;; template.html|site.conf|style.css|index.md) continue ;;
@@ -342,12 +511,13 @@ find "$src" -type d | sort | while read -r dir; do
fi fi
done done
if [ ! -f "$out/styles.css" ] && [ -f "styles/$style.css" ]; then if [ ! -f "$out/styles.css" ] && [ -f "$script_dir/styles/$style.css" ]; then
copy_style_with_resolved_vars "styles/$style.css" "$out/styles.css" copy_style_with_resolved_vars "$script_dir/styles/$style.css" "$out/styles.css"
fi fi
find "$src" -type f | sort | while IFS= read -r file; do eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while IFS= read -r file; do
rel_path="${file#$src/}" rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
dir_rel=$(dirname "$rel_path") dir_rel=$(dirname "$rel_path")
out_dir="$out/$dir_rel" out_dir="$out/$dir_rel"
@@ -355,12 +525,17 @@ find "$src" -type f | sort | while IFS= read -r file; do
template.html|site.conf|style.css|styles.css) continue ;; template.html|site.conf|style.css|styles.css) continue ;;
esac esac
if [ "$single_file_index" = "true" ] && [ "${file%.md}" != "$file" ] && [ ! -f "$(dirname "$file")/index.md" ]; then is_preserved=0
md_count=$(find "$(dirname "$file")" -maxdepth 1 -name "*.md" | wc -l) if [ -n "$(eval "find \"$file\" \( $PRESERVE_ARGS \) -print")" ]; then
is_preserved=1
fi
if [ "$single_file_index" = "true" ] && [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ] && [ ! -f "$(dirname "$file")/index.md" ]; then
md_count=$(find "$(dirname "$file")" ! -name "$(basename "$(dirname "$file")")" -prune -name "*.md" | wc -l)
[ "$md_count" -eq 1 ] && continue [ "$md_count" -eq 1 ] && continue
fi fi
if [ "${file%.md}" != "$file" ]; then if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
out_file="$out/${rel_path%.md}.html" out_file="$out/${rel_path%.md}.html"
render_markdown "$file" > "$out_file" render_markdown "$file" > "$out_file"
else else
@@ -368,4 +543,5 @@ find "$src" -type f | sort | while IFS= read -r file; do
fi fi
done done
echo "Build complete." echo "Build complete."

View File

@@ -1,754 +1,64 @@
#!/bin/sh #!/bin/sh
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk" awk_dir="$script_dir/awk"
sed_ere() {
if sed -E '' </dev/null >/dev/null 2>&1; then
sed -E "$@"
else
sed -r "$@"
fi
}
sed_ere_inplace() {
script="$1"
file="$2"
tmp="${file}.tmp.$$"
sed_ere "$script" "$file" > "$tmp" && mv "$tmp" "$file" || {
rm -f "$tmp"
return 1
}
}
sed_ere_inplace_n() {
script="$1"
file="$2"
tmp="${file}.tmp.$$"
sed_ere -n "$script" "$file" > "$tmp" && mv "$tmp" "$file" || {
rm -f "$tmp"
return 1
}
}
sed_inplace() { sed_inplace() {
script="$1" script="$1"
file="$2" file="$2"
tmp="${file}.tmp.$$" tmp="${file}.tmp.$$"
sed "$script" "$file" > "$tmp" && mv "$tmp" "$file" || { if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then
return 0
else
rm -f "$tmp" rm -f "$tmp"
return 1 return 1
} fi
} }
temp_file="/tmp/markdown.$$" temp_file="/tmp/markdown.$$.md"
cat "$@" > "$temp_file" cat "$@" > "$temp_file"
# backslash escapes for literal characters and inline code masking trap 'rm -f "$temp_file" "$temp_file.tmp"' EXIT INT TERM
awk '
function mask(s, t) {
t = s
gsub(/\*/, "\034P0\034", t)
gsub(/_/, "\034P1\034", t)
gsub(/`/, "\034P2\034", t)
gsub(/\[/, "\034P3\034", t)
gsub(/\]/, "\034P4\034", t)
gsub(/\(/, "\034P5\034", t)
gsub(/\)/, "\034P6\034", t)
gsub(/!/, "\034P7\034", t)
gsub(/\$/, "\034P8\034", t)
gsub(/#/, "\034P9\034", t)
gsub(/\+/, "\034P10\034", t)
gsub(/-/, "\034P11\034", t)
gsub(/\\/, "\034P12\034", t)
gsub(/</, "\034P13\034", t)
gsub(/>/, "\034P14\034", t)
return t
}
{
# backslash escapes
gsub(/\\\*/, "\034P0\034")
gsub(/\\_/, "\034P1\034")
gsub(/\\`/, "\034P2\034")
gsub(/\\\[/, "\034P3\034")
gsub(/\\\]/, "\034P4\034")
gsub(/\\\(/, "\034P5\034")
gsub(/\\\)/, "\034P6\034")
gsub(/\\!/, "\034P7\034")
gsub(/\\\$/, "\034P8\034")
gsub(/\\#/, "\034P9\034")
gsub(/\\\+/, "\034P10\034")
gsub(/\\\-/, "\034P11\034")
gsub(/\\\\/, "\034P12\034")
gsub(/\\</, "\034P13\034")
gsub(/\\>/, "\034P14\034")
# inline code (1 or 2 backticks) # Mask
line = $0 awk -f "$awk_dir/mask_inline_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
if (line ~ /^```/) { awk -f "$awk_dir/mask_plain.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
print line
next
}
out = ""
p = 1
while (match(substr(line, p), /`+/)) {
pstart = p + RSTART - 1
plen = RLENGTH
if (plen >= 3) {
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found 1 or 2 backticks at pstart
# Search for closing marker
marker = substr(line, pstart, plen)
tail = substr(line, pstart + plen)
mpos = index(tail, marker)
if (mpos > 0) {
# Check if it is followed by more backticks
if (substr(tail, mpos + plen, 1) == "`") {
# Not a match, treat as literal
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found match!
content = substr(tail, 1, mpos - 1)
out = out substr(line, p, pstart - p)
if (plen == 2 && substr(content, 1, 1) == " " && substr(content, length(content), 1) == " ") {
content = substr(content, 2, length(content) - 2)
}
out = out "<code>" mask(content) "</code>"
p = pstart + plen + mpos + plen - 1
} else {
# No closing marker, treat as literal
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
}
}
out = out substr(line, p)
print out
}' "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk '
function find_unescaped_tag(s, tag, p, off, pos) {
p = 1
while (1) {
off = index(substr(s, p), tag)
if (off == 0) return 0
pos = p + off - 1
if (pos == 1 || substr(s, pos - 1, 1) != "\\") return pos
p = pos + 1
}
}
function mask_plain(s, t) {
t = s
gsub(/\*/, "\034P0\034", t)
gsub(/_/, "\034P1\034", t)
gsub(/`/, "\034P2\034", t)
gsub(/\[/, "\034P3\034", t)
gsub(/\]/, "\034P4\034", t)
gsub(/\(/, "\034P5\034", t)
gsub(/\)/, "\034P6\034", t)
gsub(/!/, "\034P7\034", t)
gsub(/\$/, "\034P8\034", t)
return t
}
BEGIN { in_plain = 0 }
{
line = $0
out = ""
while (1) {
if (!in_plain) {
pos = find_unescaped_tag(line, "<plain>")
if (pos == 0) {
out = out line
break
}
out = out substr(line, 1, pos - 1) "<mfmplain>"
line = substr(line, pos + 7)
in_plain = 1
} else {
pos = find_unescaped_tag(line, "</plain>")
if (pos == 0) {
out = out mask_plain(line)
line = ""
break
}
out = out mask_plain(substr(line, 1, pos - 1)) "</mfmplain>"
line = substr(line, pos + 8)
in_plain = 0
}
}
print out
}
' "$temp_file" > "$temp_file.plain.$$" && mv "$temp_file.plain.$$" "$temp_file"
# Reference links
refs=$(cat "$@" | awk '/^\[[^\]]+\]: */')
IFS=' IFS='
' '
refs=$(sed_ere -n "/^\[.+\]: +/p" "$@") for ref in $refs; do
for ref in $refs ref_id=$(echo "$ref" | sed 's/^\[\(.*\)\]: .*/\1/')
do ref_url=$(echo "$ref" | sed 's/^\[.*\]: \([^ ]*\).*/\1/')
ref_id=$(printf %s "$ref" | sed_ere -n "s/^\[(.+)\]: .*/\1/p" | tr -d '\n') ref_title=$(echo "$ref" | sed -n 's/^\[.*\]: [^ ]* "\(.*\)"/\1/p' | sed 's@|@!@g')
ref_url=$(printf %s "$ref" | sed_ere -n "s/^\[.+\]: (.+)/\1/p" | cut -d' ' -f1 | tr -d '\n') sed_inplace "s|!\[\([^]]*\)\]\[$ref_id\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"\1\" />|g" "$temp_file"
ref_title=$(printf %s "$ref" | sed_ere -n "s/^\[.+\]: (.+) \"(.+)\"/\2/p" | sed 's@|@!@g' | tr -d '\n') sed_inplace "s|\[\([^]]*\)\]\[$ref_id\]|<a href=\"$ref_url\" title=\"$ref_title\">\1</a>|g" "$temp_file"
sed_inplace "s|!\[$ref_id\]\[\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"$ref_id\" />|g" "$temp_file"
sed_inplace "s|\[$ref_id\]\[\]|<a href=\"$ref_url\" title=\"$ref_title\">$ref_id</a>|g" "$temp_file"
done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# reference-style image using the label # Blocks
sed_ere_inplace "s|!\[([^]]+)\]\[($ref_id)\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"\1\" />|g" "$temp_file" sed_inplace "s/^>!\[/> [!/g" "$temp_file"
# reference-style link using the label sed_inplace "s/^>\[!/> [!/g" "$temp_file"
sed_ere_inplace "s|\[([^]]+)\]\[($ref_id)\]|<a href=\"$ref_url\" title=\"$ref_title\">\1</a>|g" "$temp_file"
# implicit reference-style while grep '^>' "$temp_file" >/dev/null; do
sed_ere_inplace "s|!\[($ref_id)\]\[\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"\1\" />|g" "$temp_file" awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# implicit reference-style
sed_ere_inplace "s|\[($ref_id)\]\[\]|<a href=\"$ref_url\" title=\"$ref_title\">\1</a>|g" "$temp_file"
done done
# delete the reference lines awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
sed_ere_inplace "/^\[.+\]: +/d" "$temp_file" awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# normalize GitHub admonition shorthand in blockquotes awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
sed_ere_inplace ' awk -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
/^>!\[/s/^>!\[/> [!/ awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
/^>\[!/s/^>\[!/> [!/
s/^>([^[:space:]>])/> \1/ # Spacing
' "$temp_file" awk -f "$awk_dir/breaks.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/paragraphs.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# blockquotes
# use grep to find all the nested blockquotes # Inline styles
while grep '^> ' "$temp_file" >/dev/null awk -f "$awk_dir/markdown_inline.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
do
sed_ere_inplace_n '
/^$/b blockquote
H
$ b blockquote
b
:blockquote
x
s/(\n+)(> .*)/\1<blockquote>\n\2\n<\/blockquote>/ # wrap the tags in a blockquote
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# cleanup blank lines and remove subsequent blockquote characters
sed_ere_inplace '
/^> /s/^> (.*)/\1/
' "$temp_file"
done
# convert [!TYPE] blockquotes into admonition blocks
awk '
function cap(s) { return toupper(substr(s, 1, 1)) tolower(substr(s, 2)) }
BEGIN { count = 0 }
{ lines[++count] = $0 }
END {
i = 1
while (i <= count) {
if (lines[i] == "<blockquote>") {
j = i + 1
while (j <= count && lines[j] != "</blockquote>") j++
if (j <= count) {
first = ""
first_idx = 0
for (k = i + 1; k < j; k++) {
if (lines[k] != "") {
first = lines[k]
first_idx = k
break
}
}
if (first ~ /^\[![A-Za-z]+\]$/) {
kind = first
sub(/^\[!/, "", kind)
sub(/\]$/, "", kind)
lkind = tolower(kind)
if (lkind == "note" || lkind == "tip" || lkind == "important" || lkind == "warning" || lkind == "caution") {
print "<div class=\"admonition admonition-" lkind "\">"
print "<p class=\"admonition-title\">" cap(lkind) "</p>"
has_body = 0
for (k = first_idx + 1; k < j; k++) {
if (lines[k] != "") {
print "<p>" lines[k] "</p>"
has_body = 1
}
}
if (!has_body) print "<p></p>"
print "</div>"
i = j + 1
continue
}
}
}
}
print lines[i]
i++
}
}
' "$temp_file" > "$temp_file.admon.$$" && mv "$temp_file.admon.$$" "$temp_file"
# Setext-style headers
sed_ere_inplace_n '
# Setext-style headers need to be wrapped around newlines
/^$/ b print
# else, append to holding area
H
$ b print
b
:print
x
/=+$/{
s/\n(.*)\n=+$/\n<h1>\1<\/h1>/
p
b
}
/\-+$/{
s/\n(.*)\n\-+$/\n<h2>\1<\/h2>/
p
b
}
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# atx-style headers and other block styles
sed_ere_inplace '
/^#+ /s/ #+$// # kill all ending header characters
/^# /s/# ([A-Za-z0-9 ]*)(.*)/<h1 id="\1">\1\2<\/h1>/g # H1
/^#{2} /s/#{2} ([A-Za-z0-9 ]*)(.*)/<h2 id="\1">\1\2<\/h2>/g # H2
/^#{3} /s/#{3} ([A-Za-z0-9 ]*)(.*)/<h3 id="\1">\1\2<\/h3>/g # H3
/^#{4} /s/#{4} ([A-Za-z0-9 ]*)(.*)/<h4 id="\1">\1\2<\/h4>/g # H4
/^#{5} /s/#{5} ([A-Za-z0-9 ]*)(.*)/<h5 id="\1">\1\2<\/h5>/g # H5
/^#{6} /s/#{6} ([A-Za-z0-9 ]*)(.*)/<h6 id="\1">\1\2<\/h6>/g # H6
/^\*\*\*+$/s/\*\*\*+/<hr \/>/ # hr with *
/^---+$/s/---+/<hr \/>/ # hr with -
/^___+$/s/___+/<hr \/>/ # hr with _
' "$temp_file"
# unordered lists
# use grep to find all the nested lists
while grep '^[\*\+\-] ' "$temp_file" >/dev/null
do
sed_ere_inplace_n '
# wrap the list
/^$/b list
# wrap the li tags then add to the hold buffer
# use uli instead of li to avoid collisions when processing nested lists
/^[\*\+\-] /s/[\*\+\-] (.*)/<\/uli>\n<uli>\n\1/
H
$ b list # if at end of file, check for the end of a list
b # else, branch to the end of the script
# this is where a list is checked for the pattern
:list
# exchange the hold space into the pattern space
x
# look for the list items, if there wrap the ul tags
/<uli>/{
s/(.*)/\n<ul>\1\n<\/uli>\n<\/ul>/ # close the ul tags
s/\n<\/uli>// # kill the first superfluous closing tag
p
b
}
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# convert to the proper li to avoid collisions with nested lists
sed_inplace 's/uli>/li>/g' "$temp_file"
# prepare any nested lists
sed_ere_inplace '/^[\*\+\-] /s/(.*)/\n\1\n/' "$temp_file"
done
# ordered lists
# use grep to find all the nested lists
while grep -E '^[1-9]+\. ' "$temp_file" >/dev/null
do
sed_ere_inplace_n '
# wrap the list
/^$/b list
# wrap the li tags then add to the hold buffer
# use oli instead of li to avoid collisions when processing nested lists
/^[1-9]+\. /s/[1-9]+\. (.*)/<\/oli>\n<oli>\n\1/
H
$ b list # if at end of file, check for the end of a list
b # else, branch to the end of the script
:list
# exchange the hold space into the pattern space
x
# look for the list items, if there wrap the ol tags
/<oli>/{
s/(.*)/\n<ol>\1\n<\/oli>\n<\/ol>/ # close the ol tags
s/\n<\/oli>// # kill the first superfluous closing tag
p
b
}
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# convert list items into proper list items to avoid collisions with nested lists
sed_inplace 's/oli>/li>/g' "$temp_file"
# prepare any nested lists
sed_ere_inplace '/^[1-9]+\. /s/(.*)/\n\1\n/' "$temp_file"
done
# make escaped periods literal
sed_ere_inplace '/^[1-9]+\\. /s/([1-9]+)\\. /\1\. /' "$temp_file"
# fenced code blocks (triple backticks)
awk '
BEGIN { in_fence = 0; first_line = 0 }
{
if (!in_fence && $0 ~ /^```/) {
printf "<pre><code>"
in_fence = 1
first_line = 1
next
}
if (in_fence && $0 ~ /^```[[:space:]]*$/) {
print "</code></pre>"
in_fence = 0
next
}
if (in_fence) {
if (first_line) {
first_line = 0
if ($0 == "") next
}
print
} else {
print
}
}
END {
if (in_fence) print "</code></pre>"
}
' "$temp_file" > "$temp_file.fence.$$" && mv "$temp_file.fence.$$" "$temp_file"
# code blocks
sed_ere_inplace_n '
# if at end of file, append the current line to the hold buffer and print it
${
H
b code
}
# wrap the code block on any non code block lines
/^\t| {4}/!b code
# else, append to the holding buffer and do nothing
H
b # else, branch to the end of the script
:code
# exchange the hold space with the pattern space
x
# look for the code items, if there wrap the pre-code tags
/\t| {4}/{
s/(\t| {4})(.*)/<pre><code>\n\1\2\n<\/code><\/pre>/ # wrap the ending tags
p
b
}
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# convert html characters inside pre-code tags into printable representations
sed_ere_inplace '
# get inside pre-code tags
/^<pre><code>/{
:inside
n
# if you found the end tags, branch out
/^<\/code><\/pre>/!{
s/&/\&amp;/g # ampersand
s/</\&lt;/g # less than
s/>/\&gt;/g # greater than
b inside
}
}
' "$temp_file"
# remove the first tab (or 4 spaces) from the code lines
sed_ere_inplace 's/^\t| {4}(.*)/\1/' "$temp_file"
# markdown pipe tables
awk '
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function is_table_row(line, t) {
t = line
return (t ~ /^[[:space:]]*\|/ && t ~ /\|[[:space:]]*$/)
}
function is_table_sep(line, t) {
if (!is_table_row(line)) return 0
t = line
gsub(/[|:\-[:space:]]/, "", t)
return (t == "" && line ~ /-/)
}
function split_row(line, out, n, i, raw) {
raw = line
sub(/^[[:space:]]*\|/, "", raw)
sub(/\|[[:space:]]*$/, "", raw)
n = split(raw, out, /\|/)
for (i = 1; i <= n; i++) out[i] = trim(out[i])
return n
}
function align_for(sep, t) {
t = trim(sep)
if (t ~ /^:-+:$/) return "center"
if (t ~ /^:-+$/) return "left"
if (t ~ /^-+:$/) return "right"
return ""
}
function render_cell(cell, inner) {
inner = trim(cell)
if (inner ~ /^```.*```$/) {
sub(/^```[[:space:]]*/, "", inner)
sub(/[[:space:]]*```$/, "", inner)
return "<pre><code>" inner "</code></pre>"
}
return inner
}
BEGIN { count = 0 }
{ lines[++count] = $0 }
END {
in_pre = 0
i = 1
while (i <= count) {
if (lines[i] ~ /^<pre><code>/) {
in_pre = 1
print lines[i]
i++
continue
}
if (in_pre) {
print lines[i]
if (lines[i] ~ /^<\/code><\/pre>/) in_pre = 0
i++
continue
}
if (i < count && is_table_row(lines[i]) && is_table_sep(lines[i + 1])) {
n_header = split_row(lines[i], header)
n_sep = split_row(lines[i + 1], sep)
n_cols = (n_header > n_sep ? n_header : n_sep)
print "<table>"
print "<thead>"
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_header ? render_cell(header[c]) : "")
a = (c <= n_sep ? align_for(sep[c]) : "")
if (a != "") print "<th style=\"text-align: " a ";\">" cell "</th>"
else print "<th>" cell "</th>"
}
print "</tr>"
print "</thead>"
j = i + 2
print "<tbody>"
while (j <= count && is_table_row(lines[j])) {
n_body = split_row(lines[j], body)
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_body ? render_cell(body[c]) : "")
a = (c <= n_sep ? align_for(sep[c]) : "")
if (a != "") print "<td style=\"text-align: " a ";\">" cell "</td>"
else print "<td>" cell "</td>"
}
print "</tr>"
j++
}
print "</tbody>"
print "</table>"
i = j
continue
}
if (is_table_sep(lines[i]) && i < count && is_table_row(lines[i + 1])) {
n_sep = split_row(lines[i], sep)
n_cols = n_sep
print "<table>"
print "<thead>"
print "<tr>"
for (c = 1; c <= n_cols; c++) {
a = align_for(sep[c])
if (a != "") print "<th style=\"text-align: " a ";\"></th>"
else print "<th></th>"
}
print "</tr>"
print "</thead>"
j = i + 1
print "<tbody>"
while (j <= count && is_table_row(lines[j])) {
n_body = split_row(lines[j], body)
print "<tr>"
for (c = 1; c <= n_cols; c++) {
cell = (c <= n_body ? render_cell(body[c]) : "")
a = align_for(sep[c])
if (a != "") print "<td style=\"text-align: " a ";\">" cell "</td>"
else print "<td>" cell "</td>"
}
print "</tr>"
j++
}
print "</tbody>"
print "</table>"
i = j
continue
}
print lines[i]
i++
}
}
' "$temp_file" > "$temp_file.table.$$" && mv "$temp_file.table.$$" "$temp_file"
# br tags
sed_ere_inplace '
# if an empty line, append it to the next line, then check on whether there is two in a row
/^$/ {
N
N
/^\n{2}/s/(.*)/\n<br \/>\1/
}
' "$temp_file"
# emphasis and strong emphasis and strikethrough
sed_ere_inplace_n '
# batch up the entire stream of text until a line break in the action
/^$/b emphasis
H
$ b emphasis
b
:emphasis
x
s/\*\*([^\n]+)\*\*/<strong>\1<\/strong>/g
s/__([^_\n]+)__/<strong>\1<\/strong>/g
s/\*([^\*\n]+)\*/<em>\1<\/em>/g
s/([^\\])_([^_\n]+)_/\1<em>\2<\/em>/g
s/\~\~([^\n]+)\~\~/<strike>\1<\/strike>/g
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# paragraphs
sed_ere_inplace_n '
# if an empty line, check the paragraph
/^$/ b para
# else append it to the hold buffer
H
# at end of file, check paragraph
$ b para
# now branch to end of script
b
# this is where a paragraph is checked for the pattern
:para
# return the entire paragraph into the pattern space
x
# look for non block-level elements, if there - print the p tags
/\n<(div|table|pre|p|[ou]l|h[1-6]|[bh]r|blockquote|li)/!{
s/(\n+)(.*)/\1<p>\n\2\n<\/p>/
p
b
}
p
' "$temp_file"
sed_inplace '1 d' "$temp_file" # cleanup superfluous first line
# cleanup area where P tags have broken nesting
sed_ere_inplace_n '
# if the line looks like like an end tag
/^<\/(div|table|pre|p|[ou]l|h[1-6]|[bh]r|blockquote)>/{
h
# if EOF, print the line
$ {
x
b done
}
# fetch the next line and check on whether or not it is a P tag
n
/^<\/p>/{
G
b done
}
# else, append the line to the previous line and print them both
H
x
}
:done
p
' "$temp_file"
# inline styles and special characters
sed_ere_inplace '
/^<pre><code>/,/^<\/code><\/pre>/b
s/<(http[s]?:\/\/.*)>/<a href=\"\1\">\1<\/a>/g # automatic links
s/<(.*@.*\..*)>/<a href=\"mailto:\1\">\1<\/a>/g # automatic email address links
# force-inline image syntax (double bang)
s/!!\[([^]]*)\]\(([^)]*) \"([^\"]*)\"\)/<img data-force-inline=\"1\" alt=\"\1\" src=\"\2\" title=\"\3\" \/>/g
s/!!\[([^]]*)\]\(([^)]*)\)/<img data-force-inline=\"1\" alt=\"\1\" src=\"\2\" \/>/g
s/(^|[^\\])!\[([^]]*)\]\(([^)]*) \"([^\"]*)\"\)/\1<img alt=\"\2\" src=\"\3\" title=\"\4\" \/>/g # inline image with title
s/(^|[^\\])!\[([^]]*)\]\(([^)]*)\)/\1<img alt=\"\2\" src=\"\3\" \/>/g # inline image without title
s/(^|[^\\!])\[([^]]*)\]\(([^)]*) \"([^\"]*)\"\)/\1<a href=\"\3\" title=\"\4\">\2<\/a>/g # inline link with title
s/(^|[^\\!])\[([^]]*)\]\(([^)]*)\)/\1<a href=\"\3\">\2<\/a>/g # inline link
# MFM font syntax
s/\$\[font\.serif ([^]]+)\]/<span style=\"font-family: serif;\">\1<\/span>/g
s/\$\[font\.monospace ([^]]+)\]/<span style=\"font-family: monospace;\">\1<\/span>/g
s/\$\[font\.sans ([^]]+)\]/<span style=\"font-family: sans-serif;\">\1<\/span>/g
# special characters
/&.+;/!s/&/\&amp;/g # ampersand
/<[\/a-zA-Z]/!s/</\&lt;/g# less than bracket
' "$temp_file"
# display and cleanup
awk -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -f "$awk_dir/markdown_embed.awk" "$temp_file" awk -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -f "$awk_dir/markdown_embed.awk" "$temp_file"
rm "$temp_file" rm "$temp_file"

View File

@@ -9,3 +9,8 @@ display_logo = false
display_title = true display_title = true
logo_as_favicon = true logo_as_favicon = true
favicon = "" favicon = ""
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""

BIN
site/button.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -15,3 +15,5 @@ Sed dictum tortor at interdum dignissim. Nunc hendrerit sollicitudin elementum.
Cras vitae sapien egestas, blandit libero et, volutpat augue. Ut augue quam, sollicitudin quis libero laoreet, bibendum imperdiet massa. Duis sed venenatis risus. Praesent a est mollis, viverra erat quis, faucibus elit. Donec at sagittis est, non posuere nisi. Integer posuere pharetra dui in aliquam. Morbi vehicula eros in hendrerit aliquam. Duis in turpis vel mauris mattis convallis in id tortor. Cras et aliquam augue. Cras vitae sapien egestas, blandit libero et, volutpat augue. Ut augue quam, sollicitudin quis libero laoreet, bibendum imperdiet massa. Duis sed venenatis risus. Praesent a est mollis, viverra erat quis, faucibus elit. Donec at sagittis est, non posuere nisi. Integer posuere pharetra dui in aliquam. Morbi vehicula eros in hendrerit aliquam. Duis in turpis vel mauris mattis convallis in id tortor. Cras et aliquam augue.
Cras quis consectetur dolor, a sodales tortor. Vestibulum aliquam lacinia metus, sed viverra erat egestas in. Morbi interdum sapien sed bibendum maximus. Aenean accumsan pharetra libero dapibus aliquam. Etiam sodales purus posuere gravida ullamcorper. Vestibulum tincidunt, nibh a pulvinar aliquet, leo tortor pulvinar diam, ut viverra nunc elit bibendum nulla. Praesent vel pulvinar erat, eu efficitur magna. Mauris at consequat purus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in dui quis nisi elementum aliquam. Proin eget justo sed est commodo accumsan. Suspendisse a feugiat tellus, eget gravida tellus. Cras quis consectetur dolor, a sodales tortor. Vestibulum aliquam lacinia metus, sed viverra erat egestas in. Morbi interdum sapien sed bibendum maximus. Aenean accumsan pharetra libero dapibus aliquam. Etiam sodales purus posuere gravida ullamcorper. Vestibulum tincidunt, nibh a pulvinar aliquet, leo tortor pulvinar diam, ut viverra nunc elit bibendum nulla. Praesent vel pulvinar erat, eu efficitur magna. Mauris at consequat purus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in dui quis nisi elementum aliquam. Proin eget justo sed est commodo accumsan. Suspendisse a feugiat tellus, eget gravida tellus.
![https://www.youtube.com/embed/NvQD9E5Cq8A]

View File

@@ -38,6 +38,11 @@ style = "kewt"
dir_indexes = true dir_indexes = true
single_file_index = true single_file_index = true
flatten = false flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href="https://kewt.krzak.org">kewt</a>" footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = "" logo = ""
display_logo = false display_logo = false
@@ -51,6 +56,11 @@ favicon = ""
- `dir_indexes` generate directory index pages when missing `index.md` - `dir_indexes` generate directory index pages when missing `index.md`
- `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html` - `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html`
- `flatten` flatten sidebar directory levels - `flatten` flatten sidebar directory levels
- `order` comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` text for the home link in navigation (default: "Home")
- `show_home_in_nav` show home link in navigation (default: true)
- `nav_links` comma separated extra nav links, as bare URLs or Markdown links like `[Label](https://example.com)`
- `nav_extra` raw HTML appended inside the `<nav>` after the generated link list
- `footer` footer html/text shown at the bottom of pages - `footer` footer html/text shown at the bottom of pages
- `logo` logo image path (used in header if enabled) - `logo` logo image path (used in header if enabled)
- `display_logo` show logo in header - `display_logo` show logo in header
@@ -58,6 +68,12 @@ favicon = ""
- `logo_as_favicon` use `logo` as favicon - `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set) - `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
## Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds ## Embeds
- `\![link]`: - `\![link]`:
@@ -73,6 +89,5 @@ favicon = ""
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin) - Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23) - Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
# Warning
>![WARNING] >![WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so... >Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

12
site/site.conf Normal file
View File

@@ -0,0 +1,12 @@
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
footer = "<a href="https://kewt.krzak.org"><img src="/button.gif" /></a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
order = ""

View File

@@ -43,6 +43,15 @@ header h1 {
font-style: italic; font-style: italic;
} }
.site-logo {
vertical-align: middle;
height: 1.1em;
width: auto;
border: none;
margin-right: 0.2em;
margin-top: -0.1em;
}
header a { header a {
color: var(--fg); color: var(--fg);
text-decoration: none; text-decoration: none;
@@ -199,3 +208,15 @@ footer {
margin-left: 240px; margin-left: 240px;
margin-top: 0px; margin-top: 0px;
} }
img {
image-rendering: auto;
image-rendering: crisp-edges;
image-rendering: pixelated;
image-rendering: -webkit-optimize-contrast;
}
footer img {
display: inline-block;
vertical-align: top;
}

32
tools/build-standalone.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/sh
set -e
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ ! -f "$REPO_ROOT/kewt.sh" ]; then
echo "kewt.sh not found. Run from the repository root or tools/."
exit 1
fi
OUT_FILE="$REPO_ROOT/kewt"
cat << 'EOF' > "$OUT_FILE"
#!/bin/sh
tmpdir=$(mktemp -d "/tmp/kewt.XXXXXX")
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
# Extract payload
sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir"
# Pass control to the extracted script
exec "$tmpdir/kewt.sh" "$@"
#==PAYLOAD==
EOF
tar -cz -C "$REPO_ROOT" kewt.sh markdown.sh awk styles >> "$OUT_FILE"
chmod +x "$OUT_FILE"
echo "Generated standalone executable at $OUT_FILE"