A rest argument keeps an empty first element; a removal block keeps its neighbour; the Markdown converter reads paragraphs whole; Smart App Control on the Windows page (from dev 1083db946f05)
- A rest argument (a list's or a table's items) now strips exactly ONE leading bar -- the separator from what precedes it -- so a second one is an empty first element: a table with an empty header corner is written `@table |` then ` | Maskulinum | ...`. Before, every leading bar was dropped and such a header row shifted one column left. New shipped suite tst/rest_test.sh. - Text written hard against a closing ]# (a period, a letter) is no longer removed with the block; a following space or line break still is. - sks/tns/md_to_sks.py converts inline Markdown per paragraph, so emphasis that wraps is converted and not mis-paired; bold may contain italic; tables are unnumbered; links to sibling .md files converted alongside become links to their html pages; the arrow becomes @rightarrow@. - doc/install/windows_source_install.md: Smart App Control must be off -- it blocks MSYS2's unsigned toolchain, and its Evaluation state turns itself on later.
This commit is contained in:
1 parent
6e9889f7ae
commit
bc2b3d8461
8 files changed
+244
-23
No files matched your search
+58
-8
@@ -17,15 +17,25 @@ grep. A file with no markers is inside the convertible subset.
|
||||
|
||||
First run: the Rectify user guide (1,805 lines), 2026-08-07. It rendered to
|
||||
html and pdf without error or warning, with 99 markers -- 91 of them internal
|
||||
anchor links, the one gap that appears at volume.
|
||||
anchor links, the one gap that appears at volume. Second run: three German
|
||||
vocabulary lists (4,200 lines, 152 tables), 2026-09-30, which rendered
|
||||
without an error and with two silently wrong results -- an emphasis that
|
||||
wrapped and a table's empty header corner -- so "renders clean" is not
|
||||
"converted"; the fixes are in the spec's "What the second conversion
|
||||
taught".
|
||||
|
||||
Usage: python3 md_to_sks.py <input.md> <output.kt>
|
||||
Usage: python3 md_to_sks.py <input.md> <output.kt> [sibling.md ...]
|
||||
|
||||
The siblings are Markdown files converted alongside this one: a link to
|
||||
one of them becomes a link to its html page (../<stem>/index.html, where
|
||||
ktext writes it); a link to any other .md file is a gap.
|
||||
"""
|
||||
import re, sys, os, datetime
|
||||
from collections import Counter
|
||||
|
||||
SPECIALS = "^@#|*" # ^ first: the other quotings introduce ^
|
||||
marks = Counter()
|
||||
SIBLINGS = set() # stems of the .md files converted together
|
||||
|
||||
def mark(kind, text):
|
||||
"""A #[MD ... ]# marker.
|
||||
@@ -56,7 +66,9 @@ def quote(s):
|
||||
|
||||
CODE = re.compile(r'`([^`\n]+)`')
|
||||
LINK = re.compile(r'(?<!\!)\[([^\]]*)\]\(([^)]+)\)')
|
||||
BOLD = re.compile(r'\*\*([^*]+)\*\*|__([^_]+)__')
|
||||
# A bold run may hold italic runs ("**only for *sein* and *haben***"): the
|
||||
# group admits a balanced *...* inside, so the closing ** is found past it.
|
||||
BOLD = re.compile(r'\*\*((?:[^*]|\*[^*\s][^*]*\*)+)\*\*|__([^_]+)__')
|
||||
# Markdown's underscore emphasis does not open or close INSIDE a word, and
|
||||
# technical prose is full of identifiers like *name*_rectified_1.*ext* where
|
||||
# the underscores are part of the name. So the closing _ must not be
|
||||
@@ -99,10 +111,22 @@ def inline(text):
|
||||
|
||||
def link(m):
|
||||
label, target = m.group(1), m.group(2)
|
||||
# A link that cannot be converted keeps its words and records the
|
||||
# loss. The marker goes BEFORE the words: a link is usually
|
||||
# followed by punctuation, and a marker between the words and the
|
||||
# period leaves the space before the marker standing ("words .").
|
||||
if target.startswith("#"):
|
||||
# An anchor into this document: no klammer gives an arbitrary
|
||||
# heading an anchor, so keep the words and record the loss.
|
||||
return emphasis(label) + " " + keep(mark("gap", f"link to {target}"))
|
||||
# heading an anchor.
|
||||
return keep(mark("gap", f"link to {target}")) + " " + emphasis(label)
|
||||
if target.endswith(".md") and "/" not in target:
|
||||
# Another Markdown file. "@link X.md" would be read as an
|
||||
# anchor (href="#X.md"). Converted alongside this one, its html
|
||||
# page is X/index.html beside ours; otherwise it is a gap.
|
||||
stem = target[:-3]
|
||||
if stem not in SIBLINGS:
|
||||
return keep(mark("gap", f"link to {target}, not converted")) + " " + emphasis(label)
|
||||
target = f"../{stem}/index.html"
|
||||
return opening("@link " + quote(target) + " :text ") + emphasis(label) + closing()
|
||||
|
||||
def emphasis(t):
|
||||
@@ -115,6 +139,9 @@ def inline(text):
|
||||
the wrong list: IndexError, found by converting a second document.
|
||||
"""
|
||||
t = LINK.sub(link, t)
|
||||
# An arrow is the SKS klammer (Andy, 2026-10-01): U+2192, and the
|
||||
# halfwidth U+FFEB his documents use.
|
||||
t = re.sub(r'[\u2192\uffeb]', lambda m: keep(OPEN + "@rightarrow@" + CLOSE), t)
|
||||
t = STRIKE.sub(lambda m: emphasis(m.group(1)) + " " +
|
||||
keep(mark("gap", "strikethrough")), t)
|
||||
t = BOLD.sub(lambda m: opening("@b ") + (m.group(1) or m.group(2)) + closing(), t)
|
||||
@@ -230,7 +257,9 @@ def convert(path):
|
||||
rows.append([inline(c) for c in cells(src[i])])
|
||||
i += 1
|
||||
body = " ||\n".join(" | ".join(r) for r in rows)
|
||||
opts = f" :cell_hpos {' '.join(align)}" if align and set(align) != {"l"} else ""
|
||||
# A Markdown table has no caption and no number.
|
||||
opts = " :number false"
|
||||
opts += f" :cell_hpos {' '.join(align)}" if align and set(align) != {"l"} else ""
|
||||
# A Markdown table says nothing about column widths, and
|
||||
# Klammertext's default "fit" never wraps -- a prose column then
|
||||
# runs off the page (the pdf target warns). Give the widest
|
||||
@@ -263,8 +292,28 @@ def convert(path):
|
||||
out.append("")
|
||||
continue
|
||||
|
||||
out.append(inline(line) if line.strip() else "")
|
||||
i += 1
|
||||
if not line.strip():
|
||||
out.append(""); i += 1
|
||||
continue
|
||||
|
||||
# A PARAGRAPH goes through inline() whole, its line breaks kept.
|
||||
# Converting line by line left a literal "*" on each line of an
|
||||
# emphasis that wrapped, and the leftovers then paired with EACH
|
||||
# OTHER, so the wrong words came out italic -- 33 paragraphs of the
|
||||
# vocabulary lists, visible only in a diff.
|
||||
para = [line]; i += 1
|
||||
while i < n and src[i].strip() and not (HEADING.match(src[i]) or
|
||||
TABLEROW.match(src[i]) or FENCE.match(src[i]) or
|
||||
BULLET.match(src[i])):
|
||||
if ORDERED.match(src[i]):
|
||||
# A numbered line hard against the line above is a wrapped
|
||||
# line that happens to begin with a number ("1. and 3.
|
||||
# Sg."). CommonMark reads it as a list; the writer did not
|
||||
# mean one. Kept in the paragraph, and said.
|
||||
out.append(mark("judgment", "a line beginning with a number "
|
||||
"was kept in its paragraph, not read as a list"))
|
||||
para.append(src[i]); i += 1
|
||||
out.append(inline("\n".join(para)))
|
||||
|
||||
return out, title, stripped_numbers, dropped_toc
|
||||
|
||||
@@ -312,6 +361,7 @@ def convert_list(block):
|
||||
|
||||
def main():
|
||||
src_path, dst_path = sys.argv[1], sys.argv[2]
|
||||
SIBLINGS.update(os.path.splitext(os.path.basename(a))[0] for a in sys.argv[3:])
|
||||
body, title, stripped, dropped_toc = convert(src_path)
|
||||
today = datetime.date.today().isoformat()
|
||||
head = [f"#[MD source: {os.path.abspath(src_path)}",
|
||||
|
||||
Reference in new issue
Block a user