AtlatestRepositorysigil-markdown
sigil-markdown / tree / testtest-markdown.sgl
1
(import (sigil test)2
(sigil markdown))4
;; Minimal SXML accessors for these tests. The standalone (sigil sxml)5
;; library is no longer part of this package's dependency set (SXML helpers6
;; now live in sigil-wasm-dom), so we define the few accessors the tests7
;; need locally rather than pulling in a UI dependency for testing.8
(define (sxml-tag node) (car node))10
(define (sxml-has-attrs? node)11
(and (pair? (cdr node))12
(pair? (cadr node))13
(eq? (car (cadr node)) '@)))15
(define (sxml-attributes node)16
(if (sxml-has-attrs? node)17
(cdr (cadr node))18
'()))20
(define (sxml-content node)21
(if (sxml-has-attrs? node)22
(cddr node)23
(cdr node)))25
;; ============================================================26
;; Headings27
;; ============================================================29
(test-group "headings"30
(test "h1"31
(let ((doc (markdown->sxml "# Hello")))32
(assert-equal 'h1 (sxml-tag (car (sxml-content doc))))))34
(test "h2"35
(let ((doc (markdown->sxml "## Sub")))36
(assert-equal 'h2 (sxml-tag (car (sxml-content doc))))))38
(test "h3"39
(let ((doc (markdown->sxml "### Third")))40
(assert-equal 'h3 (sxml-tag (car (sxml-content doc))))))42
(test "h6"43
(let ((doc (markdown->sxml "###### Deepest")))44
(assert-equal 'h6 (sxml-tag (car (sxml-content doc)))))))46
;; ============================================================47
;; Paragraphs48
;; ============================================================50
(test-group "paragraphs"51
(test "single paragraph"52
(let* ((doc (markdown->sxml "Hello world"))53
(content (sxml-content doc)))54
(assert-equal 'p (sxml-tag (car content)))))56
(test "multiple paragraphs separated by blank lines"57
(let* ((doc (markdown->sxml "First\n\nSecond"))58
(content (sxml-content doc)))59
(assert-equal 2 (length content))60
(assert-equal 'p (sxml-tag (car content)))61
(assert-equal 'p (sxml-tag (cadr content))))))63
;; ============================================================64
;; Code blocks65
;; ============================================================67
(test-group "code blocks"68
(test "fenced code block"69
(let* ((doc (markdown->sxml "```\ncode here\n```"))70
(content (sxml-content doc))71
(block (car content)))72
(assert-equal 'pre (sxml-tag block))))74
(test "fenced with language tag"75
(let* ((doc (markdown->sxml "```scheme\n(+ 1 2)\n```"))76
(content (sxml-content doc))77
(block (car content)))78
(assert-equal 'pre (sxml-tag block)))))80
;; ============================================================81
;; Lists82
;; ============================================================84
(test-group "lists"85
(test "unordered list"86
(let* ((doc (markdown->sxml "- one\n- two\n- three"))87
(content (sxml-content doc))88
(list-elem (car content)))89
(assert-equal 'ul (sxml-tag list-elem))))91
(test "ordered list"92
(let* ((doc (markdown->sxml "1. first\n2. second"))93
(content (sxml-content doc))94
(list-elem (car content)))95
(assert-equal 'ol (sxml-tag list-elem)))))97
;; ============================================================98
;; Inline formatting99
;; ============================================================101
(test-group "inline formatting"102
(test "bold"103
(let ((result (parse-inline "**bold**")))104
(assert-true (pair? result))105
;; Should contain a strong element106
(assert-true (pair? (filter (lambda (x)107
(and (pair? x) (eq? (car x) 'strong)))108
result)))))110
(test "italic"111
(let ((result (parse-inline "*italic*")))112
(assert-true (pair? result))113
(assert-true (pair? (filter (lambda (x)114
(and (pair? x) (eq? (car x) 'em)))115
result)))))117
(test "inline code"118
(let ((result (parse-inline "`code`")))119
(assert-true (pair? result))120
(assert-true (pair? (filter (lambda (x)121
(and (pair? x) (eq? (car x) 'code)))122
result)))))124
(test "link"125
(let ((result (parse-inline "[text](http://example.com)")))126
(assert-true (pair? result))127
(assert-true (pair? (filter (lambda (x)128
(and (pair? x) (eq? (car x) 'a)))129
result)))))131
(test "image"132
(let ((result (parse-inline "")))133
(assert-true (pair? result))134
(assert-true (pair? (filter (lambda (x)135
(and (pair? x) (eq? (car x) 'img)))136
result))))))138
;; ============================================================139
;; Intraword underscores are not emphasis delimiters140
;; ============================================================141
;;142
;; An underscore between two word characters is ordinary text, per143
;; CommonMark. Before this rule existed, every snake_case identifier written144
;; as prose was silently eaten: `sokol_gfx and sokol_gp` became145
;; `sokol<em>gfx and sokol</em>gp`, live on a public site.146
;;147
;; THE STRINGS BELOW ARE COPIED FROM THE REAL CORPUS -- Azoth estate READMEs148
;; and commit messages -- not invented. A case written from the same149
;; understanding that produced the bug tends to satisfy the buggy rule too;150
;; these cannot drift from what the estate actually renders.151
;;152
;; The assertion is TEXT-IDENTITY, not "no em node present". Absence of a153
;; symptom passes for free if the parser drops the input entirely; equality154
;; with the source string does not.156
(define (inline-text result)157
(let flatten ((n result) (acc ""))158
(cond ((string? n) (string-append acc n))159
((and (pair? n) (symbol? (car n)))160
;; skip the tag, and an (@ ...) attribute list if present161
(let ((rest (if (and (pair? (cdr n)) (pair? (cadr n))162
(eq? (car (cadr n)) '@))163
(cddr n)164
(cdr n))))165
(flatten rest acc)))166
((pair? n) (flatten (cdr n) (flatten (car n) acc)))167
(else acc))))169
(define (assert-literal source)170
(assert-equal source (inline-text (parse-inline source))))172
(test-group "intraword underscores"173
;; --- from README.md of sigil-graphics, rendered wrong on azoth.works ---174
(test "sokol_gfx and sokol_gp survive"175
(assert-literal "Provides 2D rendering capabilities via sokol_gfx and sokol_gp"))177
(test "stb_image and stb_truetype survive"178
(assert-literal "image loading (stb_image), and font rendering (stb_truetype)."))180
;; --- from real commit messages in the estate ---181
(test "sigil_wasm_gles3 survives"182
(assert-literal "sigil_wasm_gles3 and sigil_wasm_native_start"))184
(test "KILN_ARTIFACTS survives"185
(assert-literal "KILN_ARTIFACTS and KILN_DEPS"))187
(test "group_id, node_id, run_groups survive"188
(assert-literal "group_id, node_id, plan_form and run_groups"))190
;; --- the boundary cases the rule must NOT break ---191
(test "__init__ is still strong"192
(let ((result (parse-inline "__init__ method")))193
(assert-true (pair? (filter (lambda (x) (and (pair? x) (eq? (car x) 'strong)))194
result)))195
(assert-equal "init method" (inline-text result))))197
(test "_emphasis_ at a word boundary still works"198
(let ((result (parse-inline "an _emphasis_ here")))199
(assert-true (pair? (filter (lambda (x) (and (pair? x) (eq? (car x) 'em)))200
result)))201
(assert-equal "an emphasis here" (inline-text result))))203
;; CommonMark: _foo_bar_ is <em>foo_bar</em>. The inner underscore is204
;; intraword and the outer pair is not, so the run closes at the LAST one.205
(test "_foo_bar_ emphasises across the intraword underscore"206
(let ((result (parse-inline "_foo_bar_")))207
(assert-true (pair? (filter (lambda (x) (and (pair? x) (eq? (car x) 'em)))208
result)))209
(assert-equal "foo_bar" (inline-text result))))211
;; The '*' rule is deliberately DIFFERENT and must not change with it.212
;; This is the negative control: had the fix been applied to both213
;; characters, this test goes red.214
(test "intraword ASTERISK is still emphasis"215
(let ((result (parse-inline "foo*bar*baz")))216
(assert-true (pair? (filter (lambda (x) (and (pair? x) (eq? (car x) 'em)))217
result)))218
(assert-equal "foobarbaz" (inline-text result)))))220
;; ============================================================221
;; Front matter222
;; ============================================================224
(test-group "front matter"225
(test "YAML front matter parsed"226
(let* ((doc (markdown->sxml "---\ntitle: Hello\n---\n\n# Content"))227
(attrs (sxml-attributes doc)))228
(assert-true (pair? attrs))))230
(test "no front matter returns empty attributes"231
(let* ((doc (markdown->sxml "# Just a heading"))232
(attrs (sxml-attributes doc)))233
(assert-equal '() attrs))))235
;; ============================================================236
;; Integration237
;; ============================================================239
(test-group "markdown->sxml integration"240
(test "full document"241
(let ((doc (markdown->sxml "# Title\n\nA paragraph.\n\n- item 1\n- item 2")))242
(assert-equal 'document (sxml-tag doc))243
(assert-true (>= (length (sxml-content doc)) 3)))))245
(run-tests)247
;; ============================================================248
;; Nested lists249
;;250
;; These pin BOTH meanings of "nested" that this package's history has251
;; confused. Twice the word appeared in docs and in the 0.9.2 perf252
;; investigation meaning INLINE nesting -- emphasis inside emphasis, a253
;; strong run inside a list item -- which made nested LISTS look covered254
;; when they were never implemented at all.255
;;256
;; The flattening was inherited from the original PEG parser. 0.9.2's257
;; hand-written scanners were gated on byte-identical output against258
;; goldens generated from that parser, so they reproduced the defect259
;; faithfully. The goldens were not wrong about what the old parser did;260
;; the old parser was wrong.261
;; ============================================================263
(define (li-tags doc)264
;; tags of the direct children of the first list in the document265
(map (lambda (n) (if (pair? n) (sxml-tag n) n))266
(sxml-content (car (sxml-content doc)))))268
(define (nth-li doc n)269
(list-ref (sxml-content (car (sxml-content doc))) n))271
(test-group "nested lists"272
(test "flat list stays flat"273
(let ((doc (markdown->sxml "- one\n- two\n- three")))274
(assert-equal 'ul (sxml-tag (car (sxml-content doc))))275
(assert-equal 3 (length (sxml-content (car (sxml-content doc)))))))277
(test "two-level unordered nests"278
(let* ((doc (markdown->sxml "- top\n - inner\n- back"))279
(ul (car (sxml-content doc)))280
(items (sxml-content ul)))281
;; two TOP-LEVEL items, not three siblings282
(assert-equal 2 (length items))283
;; the first item carries a nested ul284
(assert-equal 'ul (sxml-tag (car (filter pair? (cdr (car items))))))))286
(test "three-level unordered nests"287
(let* ((doc (markdown->sxml "- a\n - b\n - c\n- d"))288
(items (sxml-content (car (sxml-content doc)))))289
(assert-equal 2 (length items))))291
(test "ordered nested under unordered keeps its type"292
(let* ((doc (markdown->sxml "- top\n 1. first\n 2. second"))293
(items (sxml-content (car (sxml-content doc))))294
(inner (car (filter pair? (cdr (car items))))))295
(assert-equal 1 (length items))296
(assert-equal 'ol (sxml-tag inner))))298
;; THE CASE THAT MISLED EVERYONE: inline nesting and list nesting in one299
;; document. Both must work, and they are different things.300
(test "inline emphasis and a nested list coexist"301
(let* ((doc (markdown->sxml "- **bold** item\n - *inner* item"))302
(items (sxml-content (car (sxml-content doc))))303
(top (car items))304
(inner (car (filter pair? (cdr top)))))305
(assert-equal 1 (length items))306
;; inline nesting still works on the parent307
(assert-equal 'strong (sxml-tag (car (filter pair? (cdr top)))))308
;; ...and is not confused with the nested list309
(assert-equal 'ul (sxml-tag (list-ref (filter pair? (cdr top)) 1)))))310
;; Nesting is decided by the parent ITEM's CONTENT column, not by the311
;; list's marker indent. "10. " puts content at column 4, so a dash at312
;; column 2 is NOT inside it and must start its own list. A first cut of313
;; this fix compared against the list indent instead and silently314
;; swallowed the dash list into the ordered item -- caught only by the315
;; golden corpus, because every unit test here used a plain 2-space316
;; indent that both rules satisfy.317
(test "shallower item of another type is not absorbed"318
(let* ((doc (markdown->sxml "10. wide marker\n - dash at two"))319
(blocks (sxml-content doc)))320
(assert-equal 2 (length blocks))321
(assert-equal 'ol (sxml-tag (car blocks)))322
(assert-equal 'ul (sxml-tag (cadr blocks)))))324
(test "item reaching the content column does nest"325
(let* ((doc (markdown->sxml "10. wide marker\n - dash at four"))326
(blocks (sxml-content doc)))327
(assert-equal 1 (length blocks))328
(assert-equal 'ol (sxml-tag (car blocks)))))330
;; The dedent is load-bearing and was NOT covered until a sabotage run331
;; showed the suite staying green with it removed. Four-space nesting is332
;; common, and 4+ leading spaces is also the INDENTED CODE BLOCK trigger:333
;; without dedenting before re-parsing, " - b" becomes a (pre ...)334
;; instead of a nested list. Two-space nesting never exercises this,335
;; which is why every earlier test missed it.336
(test "four-space nesting is a list, not a code block"337
(let* ((doc (markdown->sxml "- a\n - b"))338
(top (car (sxml-content (car (sxml-content doc)))))339
(inner (car (filter pair? (cdr top)))))340
(assert-equal 'ul (sxml-tag inner))341
(assert-equal 'li (sxml-tag (car (sxml-content inner))))))343
(test "deep four-space nesting stays a list"344
(let* ((doc (markdown->sxml "- a\n - b\n - c"))345
(top (car (sxml-content (car (sxml-content doc)))))346
(inner (car (filter pair? (cdr top))))347
(deeper (car (filter pair? (cdr (car (sxml-content inner)))))))348
(assert-equal 'ul (sxml-tag inner))349
(assert-equal 'ul (sxml-tag deeper))))351
;; HTML comments. Before 0.9.4 these fell through to the paragraph case and352
;; rendered as escaped VISIBLE TEXT — which published a RELEASES.md authoring353
;; note onto usesigil.org, from a comment whose own first line read "this354
;; comment is not published".355
;;356
;; The two CONTROL tests below are the point: it is easy to write a stripper357
;; that also eats ordinary prose containing angle brackets, and a suite that358
;; only checked "the comment is gone" would pass for a parser that deleted359
;; far too much.361
(test "multi-line HTML comment is not rendered"362
(let ((doc (markdown->sxml "# T\n\n<!--\n hidden note\n-->\n\nVisible.\n")))363
(assert-equal '(document (h1 "T") (p "Visible.")) doc)))365
(test "single-line HTML comment is not rendered"366
(let ((doc (markdown->sxml "# T\n\n<!-- hidden -->\n\nVisible.\n")))367
(assert-equal '(document (h1 "T") (p "Visible.")) doc)))369
(test "CONTROL: prose containing angle brackets survives"370
(let ((doc (markdown->sxml "# T\n\na < b and c > d\n")))371
(assert-equal '(document (h1 "T") (p "a < b and c > d")) doc)))373
(test "CONTROL: a document with no comment is unchanged"374
(let ((doc (markdown->sxml "# T\n\nVisible only.\n")))375
(assert-equal '(document (h1 "T") (p "Visible only.")) doc)))377
;; CommonMark: an unterminated comment runs to end of document. Asserted so378
;; the behaviour is deliberate and discoverable rather than a surprise.379
(test "unterminated comment consumes the rest, per CommonMark"380
(let ((doc (markdown->sxml "# T\n\n<!-- oops\n\nSwallowed.\n")))381
(assert-equal '(document (h1 "T")) doc))))