Commit9fd3c675Recorded12 Jan 2026Repositorysigil-sxml

sigil-sxml: Add sxml->html and API review

Message
  • Add sxml->html for HTML5 output (void elements without trailing slash)
  • Handle document wrapper from markdown->sxml (strips wrapper)
  • Refactor sxml->xml and sxml->html to share sxml->markup internal
  • Add API review in notes/review/sigil-sxml.md
Changed
 src/sigil/sxml.sgl | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------
 1 file changed, 91 insertions(+), 49 deletions(-)
Diff
src/sigil/sxml.sglmodified
@@ -1,7 +1,7 @@
1
;;; (sigil sxml) - SXML to XML Conversion
+1
;;; (sigil sxml) - SXML to XML/HTML Conversion
2
;;;
3
;;; SXML is a standard S-expression representation of XML/HTML documents.
4
;;; This library converts SXML to XML strings for output.
+4
;;; This library converts SXML to XML or HTML strings for output.
5
;;;
6
;;; ## SXML Format
7
;;;
@@ -16,26 +16,32 @@
16
;;; ```scheme
17
;;; (import (sigil sxml))
18
;;;
19
;;; (sxml->xml '(html (head (title "Hello"))))
+19
;;; ;; HTML output (recommended for web pages)
+20
;;; (sxml->html '(html (head (title "Hello"))))
21
;;; ; => "<html><head><title>Hello</title></head></html>"
22
;;;
+23
;;; ;; XML output (for XML documents)
24
;;; (sxml->xml '(div (@ (class "main")) "Hello"))
25
;;; ; => "<div class=\"main\">Hello</div>"
26
;;; ```
27
;;;
28
;;; ## Void Elements
29
;;;
28
;;; Self-closing elements like `<br>`, `<img>`, `<input>` are handled
29
;;; automatically:
+30
;;; Void elements like `<br>`, `<img>`, `<input>` are handled automatically.
+31
;;; The output format differs between HTML and XML:
32
;;;
33
;;; ```scheme
32
;;; (sxml->xml '(img (@ (src "photo.jpg") (alt "Photo"))))
33
;;; ; => "<img src=\"photo.jpg\" alt=\"Photo\" />"
+34
;;; (sxml->html '(img (@ (src "photo.jpg"))))
+35
;;; ; => "<img src=\"photo.jpg\">"
+36
;;;
+37
;;; (sxml->xml '(img (@ (src "photo.jpg"))))
+38
;;; ; => "<img src=\"photo.jpg\" />"
39
;;; ```
40
41
(define-library (sigil sxml)
42
(export
43
;; Main conversion
+44
sxml->html ; Convert SXML to HTML string
45
sxml->xml ; Convert SXML to XML string
46
47
;; Helpers
@@ -224,28 +230,88 @@
230
(define (raw-text-element? tag)
231
(memq tag raw-text-elements))
232
227
;;; Convert SXML content item to XML string
228
;;; When raw? is #t, string content is not escaped (for style/script)
229
(define (content->xml item . raw?)
230
(let ((raw (and (pair? raw?) (car raw?))))
231
(cond
232
((string? item)
233
(if raw item (xml-escape item)))
234
((number? item) (number->string item))
235
;; *raw* element: output content without escaping
236
((and (pair? item) (eq? (car item) '*raw*))
237
(if (null? (cdr item)) "" (cadr item)))
238
((sxml-element? item) (sxml->xml item))
239
((pair? item)
240
;; Could be a list of items
241
(string-join (map (lambda (i) (content->xml i raw)) item)))
242
(else ""))))
243
233
;; ========== Conversion ==========
234
+235
;; Internal: convert SXML content item
+236
;; mode is 'html or 'xml, raw? controls escaping for style/script
+237
(define (content->markup item mode raw?)
+238
(cond
+239
((string? item)
+240
(if raw? item (xml-escape item)))
+241
((number? item) (number->string item))
+242
;; *raw* element: output content without escaping
+243
((and (pair? item) (eq? (car item) '*raw*))
+244
(if (null? (cdr item)) "" (cadr item)))
+245
((sxml-element? item) (sxml->markup item mode))
+246
((pair? item)
+247
;; Could be a list of items
+248
(string-join (map (lambda (i) (content->markup i mode raw?)) item)))
+249
(else "")))
+250
+251
;; Internal: convert SXML tree to markup string
+252
;; mode is 'html or 'xml
+253
(define (sxml->markup sxml mode)
+254
(cond
+255
;; String content
+256
((string? sxml) (xml-escape sxml))
+257
;; Number
+258
((number? sxml) (number->string sxml))
+259
;; Element
+260
((sxml-element? sxml)
+261
(let* ((tag (sxml-tag sxml))
+262
(content (sxml-content sxml)))
+263
(cond
+264
;; Document wrapper - just render children (from markdown->sxml)
+265
((eq? tag 'document)
+266
(string-join (map (lambda (c) (sxml->markup c mode)) content)))
+267
;; Void element - HTML uses <br>, XML uses <br />
+268
((and (null? content) (void-element? tag))
+269
(let ((attr-str (attributes->xml (sxml-attributes sxml))))
+270
(if (eq? mode 'html)
+271
(string-append "<" (symbol->string tag) attr-str ">")
+272
(string-append "<" (symbol->string tag) attr-str " />"))))
+273
;; Normal element with content
+274
(else
+275
(let* ((tag-str (symbol->string tag))
+276
(attrs (sxml-attributes sxml))
+277
(attr-str (attributes->xml attrs))
+278
(raw (raw-text-element? tag)))
+279
(string-append "<" tag-str attr-str ">"
+280
(string-join (map (lambda (c) (content->markup c mode raw)) content))
+281
"</" tag-str ">"))))))
+282
;; List of items (flatten)
+283
((pair? sxml)
+284
(string-join (map (lambda (s) (sxml->markup s mode)) sxml)))
+285
;; Anything else
+286
(else "")))
+287
+288
;;; Convert an SXML tree to an HTML string.
+289
;;;
+290
;;; Handles nested elements, attributes, text content, and void elements.
+291
;;; Void elements like `<br>` and `<img>` are output without a closing slash.
+292
;;; Content inside `<style>` and `<script>` tags is not escaped.
+293
;;; A `document` wrapper element (from `markdown->sxml`) is stripped.
+294
;;;
+295
;;; ```scheme
+296
;;; (sxml->html '(p "Hello"))
+297
;;; ; => "<p>Hello</p>"
+298
;;;
+299
;;; (sxml->html '(div (@ (class "main")) (p "text")))
+300
;;; ; => "<div class=\"main\"><p>text</p></div>"
+301
;;;
+302
;;; (sxml->html '(img (@ (src "foo.png"))))
+303
;;; ; => "<img src=\"foo.png\">"
+304
;;;
+305
;;; (sxml->html '(document (p "From markdown")))
+306
;;; ; => "<p>From markdown</p>"
+307
;;; ```
+308
(define (sxml->html sxml)
+309
(sxml->markup sxml 'html))
+310
311
;;; Convert an SXML tree to an XML string.
312
;;;
313
;;; Handles nested elements, attributes, text content, and void elements.
+314
;;; Void elements are self-closing with ` />` (XML style).
315
;;; Content inside `<style>` and `<script>` tags is not escaped.
316
;;;
317
;;; ```scheme
@@ -259,28 +325,4 @@
325
;;; ; => "<img src=\"foo.png\" />"
326
;;; ```
327
(define (sxml->xml sxml)
262
(cond
263
;; String content
264
((string? sxml) (xml-escape sxml))
265
;; Number
266
((number? sxml) (number->string sxml))
267
;; Element
268
((sxml-element? sxml)
269
(let* ((tag (sxml-tag sxml))
270
(tag-str (symbol->string tag))
271
(attrs (sxml-attributes sxml))
272
(content (sxml-content sxml))
273
(attr-str (attributes->xml attrs))
274
(raw (raw-text-element? tag)))
275
(if (and (null? content) (void-element? tag))
276
;; Self-closing void element
277
(string-append "<" tag-str attr-str " />")
278
;; Normal element with content
279
(string-append "<" tag-str attr-str ">"
280
(string-join (map (lambda (c) (content->xml c raw)) content))
281
"</" tag-str ">"))))
282
;; List of items (flatten)
283
((pair? sxml)
284
(string-join (map content->xml sxml)))
285
;; Anything else
286
(else "")))))
+328
(sxml->markup sxml 'xml))))