perf(blocks): hand-write block classifiers; drop unused sigil-peg dep
The block-line classifiers (parse-atx-header, fenced-code-start?, fence-info, parse-list-item) were the last PEG consumers and, after the inline rewrite, the dominant render cost: the grammars' (* any-char) tail cost O(line length) per call (~90us to classify one heading line). Replace them with short hand-written scans producing byte-identical results, verified against golden snapshots of the original parser over a block edge-case corpus (7+ hashes, no-space markers, tilde/4-backtick/indented fences, multi-digit and paren-style ordered markers, indented items).
With that, (sigil peg) is no longer used anywhere in the parser, so the sigil-peg dependency is removed entirely (smaller wasm bundle in Slate).
Final native speedup vs original baseline: mixed 2.6KB 161 -> 1.70 ms (95x) large 26KB 1797 -> 16.5 ms (108x) render cost is linear at ~650 us/KB from 2.6KB to 104KB (no O(n^2)).
Also makes test/test-markdown.sgl self-contained (it imported the removed (sigil sxml) library); defines the 3 SXML accessors it needs locally. All 18 tests pass.
bench/check.sgl | 3 ++-
bench/corpus/block-edge.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
bench/golden/block-edge.sxml | 1 +
package.sgl | 3 +--
sigil.lock | 5 -----
src/sigil/markdown.sgl | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------
test/test-markdown.sgl | 24 ++++++++++++++++++++++--
7 files changed, 160 insertions(+), 63 deletions(-)bench/check.sglmodified
(sigil string))(define corpus (list "mixed" "inline-edge" "elements" "emphasis-edge" "code-link-edge")) (list "mixed" "inline-edge" "elements" "emphasis-edge" "code-link-edge" "block-edge"))(ensure-directory "bench/golden")bench/corpus/block-edge.mdadded
####### Seven hashes is not a header#NoSpaceAfterHash is not a header###### Six hashes is a header#Paragraph then####### seven hashes mid-paragraph stays paragraph-no space after dash- space after dash is a list-*no space star+ plus with space1.no space after dot1. ordered with space10. multi digit ordered1) paren ordered is not a list - indented dash item - deeper indented itemText before list- item right after paragraph~~~tilde fence no lang~~~~~~rubytilde fence with lang~~~````four backtick fence```````jsfence with lang js``` ```indented three-space fence ```> quote level one>quote no space after gt> quote with **bold** and `code`- - -* * *Regular ending paragraph with trailing text.bench/golden/block-edge.sxmladded
(document (p "####### Seven hashes is not a header #NoSpaceAfterHash is not a header") (h6 "Six hashes is a header") (p "#") (p "Paragraph then ####### seven hashes mid-paragraph stays paragraph") (p "-no space after dash") (ul (li "space after dash is a list - " "*" "no space star") (li "plus with space 1.no space after dot")) (ol (li "ordered with space") (li "multi digit ordered 1) paren ordered is not a list ")) (ul (li "indented dash item") (li "deeper indented item")) (p "Text before list") (ul (li "item right after paragraph")) (pre "tilde fence no lang") (pre (@ (lang "ruby")) "tilde fence with lang") (pre "four backtick fence") (pre (@ (lang "js")) "fence with lang js") (pre "indented three-space fence") (blockquote "quote level one quote no space after gt quote with " (strong "bold") " and " (code "code")) (hr) (hr) (p "Regular ending paragraph with trailing text.")) No newline at end of filepackage.sglmodified
optimize: 2)) dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.9.0") (from-git url: "codeberg:sigil/sigil-peg" version: "^0.9.0"))) (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.9.0")))sigil.lockmodified
sha: "54f66e8ec446b65b75ab6f12454d2c0aa269f2c0" package-selector: "sigil-stdlib" version: "0.9.1") (package name: "sigil-peg" url: "codeberg:sigil/sigil-peg" ref: "^0.9.0" sha: "2a0adbd32b13fb835cf021a8e279341161a77436" version: "0.9.1"))src/sigil/markdown.sglmodified
(define-library (sigil markdown) (import (sigil string) (sigil peg) (sigil io)) (export ;; Main API (cons parsed metadata) metadata)))))))) ;; ========== PEG Patterns for Block Parsing ========== ;; ATX header: 1-6 '#' followed by space and text (define header-pattern '(seq (<- (between 1 6 "#")) " " (<- (* any-char)))) ;; Fenced code block start: 3+ backticks or tildes with optional language (define fence-start-pattern '(seq (* " ") (/ (between 3 100 "`") (between 3 100 "~")) (* any-char))) ;; Extract language info from fence line (define fence-info-pattern '(seq (* " ") (/ (+ "`") (+ "~")) (<- (* any-char)))) ;; Unordered list item: optional spaces, marker (- + *), space, text (define unordered-item-pattern '(seq (<- (* " ")) (<- (/ "-" "+" "*")) " " (<- (* any-char)))) ;; Ordered list item: optional spaces, digits, period, space, text (define ordered-item-pattern '(seq (<- (* " ")) (<- (+ (char-range #\0 #\9))) ". " (<- (* any-char)))) ;; ========== Block Line Classification ========== ;; Parse ATX header (# Header) using PEG ;; ;; These classifiers were previously driven by the interpreted PEG ;; engine, whose `(* any-char)` tail cost O(line length) per call ;; (~90us to classify one heading line) — the dominant block-parsing ;; cost once the inline scanner landed. They are now short hand-written ;; scans producing byte-identical results (verified against golden ;; snapshots of the original PEG-based parser over the block edge-case ;; corpus: 7+ hashes, no-space markers, tilde/4-backtick/indented fences, ;; multi-digit and paren-style ordered markers, indented items). ;; Parse an ATX header line: 1-6 leading '#' then a single space then ;; text. Returns (header LEVEL TRIMMED-TEXT) or #f. Seven-plus hashes or ;; a missing space after the hashes is not a header (matches the greedy, ;; non-backtracking `(between 1 6 "#")` followed by a literal space). (define (parse-atx-header line) (let ((m (peg/match header-pattern line))) (if m (let ((caps (peg-match-captures m))) (list 'header (string-length (car caps)) (string-trim (cadr caps)))) #f))) ;; Check if line starts a fenced code block using PEG (let ((len (string-length line))) (let loop ((n 0)) (cond ((and (< n 6) (< n len) (eq? (string-ref line n) #\#)) (loop (+ n 1))) ((and (>= n 1) (< n len) (eq? (string-ref line n) #\space)) (list 'header n (string-trim (substring line (+ n 1) len)))) (else #f))))) ;; Skip leading spaces; return the index of the first non-space char. (define (skip-spaces line i len) (if (and (< i len) (eq? (string-ref line i) #\space)) (skip-spaces line (+ i 1) len) i)) ;; Count a run of CH starting at K; return the index past the run. (define (skip-run line k len ch) (if (and (< k len) (eq? (string-ref line k) ch)) (skip-run line (+ k 1) len ch) k)) ;; Does LINE start a fenced code block? Optional leading spaces, then 3+ ;; backticks or 3+ tildes. (define (fenced-code-start? line) (if (peg/match fence-start-pattern line) #t #f)) (let* ((len (string-length line)) (i (skip-spaces line 0 len))) (or (>= (- (skip-run line i len #\`) i) 3) (>= (- (skip-run line i len #\~) i) 3)))) ;; Get fence info (language) from opening fence using PEG ;; Extract the info string (language) from an opening fence: skip leading ;; spaces and the run of fence chars, then trim the remainder. (define (fence-info line) (let ((m (peg/match fence-info-pattern line))) (if m (string-trim (car (peg-match-captures m))) (let* ((len (string-length line)) (i (skip-spaces line 0 len)) (ch (cond ((and (< i len) (eq? (string-ref line i) #\`)) #\`) ((and (< i len) (eq? (string-ref line i) #\~)) #\~) (else #f)))) (if ch (string-trim (substring line (skip-run line i len ch) len)) ""))) ;; Check if line is a list item using PEG, return (type marker indent rest) or #f ;; Classify a list-item line. Returns (unordered MARKER-CHAR INDENT REST) ;; or (ordered DIGIT-STRING INDENT REST) or #f. INDENT is the leading ;; space count; a single space (unordered) or ". " (ordered) must follow ;; the marker, else it is not a list item. (define (parse-list-item line) (let ((m (peg/match unordered-item-pattern line))) (if m (let ((caps (peg-match-captures m))) (list 'unordered (string-ref (cadr caps) 0) (string-length (car caps)) (caddr caps))) (let ((m2 (peg/match ordered-item-pattern line))) (if m2 (let ((caps (peg-match-captures m2))) (list 'ordered (cadr caps) (string-length (car caps)) (caddr caps))) #f))))) (let* ((len (string-length line)) (i (skip-spaces line 0 len))) (cond ;; Unordered: marker (- + *) then a single space ((and (< i len) (let ((c (string-ref line i))) (or (eq? c #\-) (eq? c #\+) (eq? c #\*))) (< (+ i 1) len) (eq? (string-ref line (+ i 1)) #\space)) (list 'unordered (string-ref line i) i (substring line (+ i 2) len))) ;; Ordered: one or more digits then ". " (else (let ((k (let scan ((k i)) (if (and (< k len) (char-numeric? (string-ref line k))) (scan (+ k 1)) k)))) (if (and (> k i) (< (+ k 1) len) (eq? (string-ref line k) #\.) (eq? (string-ref line (+ k 1)) #\space)) (list 'ordered (substring line i k) i (substring line (+ k 2) len)) #f)))))) ;; ========== Table Parsing ==========test/test-markdown.sglmodified
(import (sigil test) (sigil markdown) (sigil sxml)) (sigil markdown));; Minimal SXML accessors for these tests. The standalone (sigil sxml);; library is no longer part of this package's dependency set (SXML helpers;; now live in sigil-wasm-dom), so we define the few accessors the tests;; need locally rather than pulling in a UI dependency for testing.(define (sxml-tag node) (car node))(define (sxml-has-attrs? node) (and (pair? (cdr node)) (pair? (cadr node)) (eq? (car (cadr node)) '@)))(define (sxml-attributes node) (if (sxml-has-attrs? node) (cdr (cadr node)) '()))(define (sxml-content node) (if (sxml-has-attrs? node) (cddr node) (cdr node)));; ============================================================;; Headings