Skip to content

Commit e9ed4f8

Browse files
author
pedro
committed
v1.0.0: Complete architectural refactor with 10 output formats
ARCHITECTURE: - Rewrote filter around two-stage pipeline (Normalize → Write) - Replaced single-function approach with modular reader/writer pattern - Introduced FormatDefaults table and dispatcher pattern - Added TypeCase and Container enums for type safety BREAKING CHANGES: - Metadata key redesigned: alerts-normalize now accepts string or nested map - Removed quarto-format/pandoc-format boolean flags ADDED: - 10 output formats (Quarto, Pandoc, GitHub, Obsidian, MkDocs, MyST, Sphinx, Hugo, Docusaurus, VitePress) - pandoc-md intermediate format for round-trip pipelines - Collapse support with +/- markers - Inline title capture from marker line - Case-insensitive type matching - Quarto div normalizer (:::{.callout-*}) - Pandoc 3.9 / Sphinx normalizer support - 25 built-in callout types - custom-types frontmatter option for extensibility CHANGED: - Title handling now matches pandoc 3.9 semantics - write_blockquote uses pure AST for correct round-trip parsing - Eliminated magic strings in favor of enums
1 parent 5aba05e commit e9ed4f8

36 files changed

+1330
-375
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
- name: Run alerts-normalize tests (Quarto format)
2323
run: make test-quarto
2424

25+
- name: Run alerts-normalize roundtrip tests
26+
run: make test-roundtrip
27+
2528
test-latest:
2629
runs-on: ubuntu-latest
2730
steps:
@@ -35,18 +38,18 @@ jobs:
3538
- name: Run alerts-normalize tests (Quarto format)
3639
run: make test-quarto
3740

41+
- name: Run alerts-normalize roundtrip tests
42+
run: make test-roundtrip
43+
3844
test-quarto:
3945
runs-on: ubuntu-latest
4046
steps:
4147
- uses: actions/checkout@v4
4248

43-
- name: Install LaTeX packages
44-
run: sudo apt-get install -y texlive-fonts-extra texlive-luatex context
45-
4649
- uses: quarto-dev/quarto-actions/setup@v2
4750

4851
- name: Run alerts-normalize tests (Quarto + pandoc-format)
4952
run: make test-quarto-pandoc
5053

51-
- name: Run alerts-normalize example (all formats)
52-
run: cd examples && make all
54+
- name: Run alerts-normalize roundtrip tests
55+
run: make test-roundtrip

CHANGELOG.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Changelog
2+
3+
## \[1.0.0]
4+
5+
### Architecture
6+
7+
The filter was rewritten from scratch around a two-stage pipeline:
8+
9+
* **Stage 1 — Normalize**: each reader (`normalize_github`, `normalize_div`) converts its input format into a canonical intermediate div with `kind`, `title`, and `collapse` attributes. No output format knowledge needed.
10+
* **Stage 2 — Write**: `write_callout` dispatches to the correct writer via a `Writers` table keyed by `Container` enum. Each writer only knows its own syntax.
11+
12+
This replaces the original single-function approach (`make_div`) that mixed reading, normalizing, and writing into one path with two hardcoded outputs. The new design makes adding a format a matter of adding one row to `FormatDefaults` and one writer function, with no changes to existing code.
13+
14+
### Breaking changes
15+
16+
* Metadata key redesigned. The old boolean-flag form (`quarto-format: true`) is replaced by a clean string or nested map:
17+
18+
```yaml
19+
# simple string — works on command line and in frontmatter
20+
alerts-normalize: pandoc-format
21+
22+
# nested map — for additional options
23+
alerts-normalize:
24+
out-format: pandoc-format
25+
custom-types:
26+
- spoiler
27+
- exercise
28+
```
29+
30+
### Added
31+
32+
* **10 output formats**: `quarto-format`, `pandoc-format`, `pandoc-md`, `github-format`, `obsidian-format`, `mkdocs-format`, `myst-format`, `sphinx-format`, `hugo-format`, `docusaurus-format`, `vitepress-format`.
33+
* **`pandoc-md` intermediate format**: attribute-based div representation for round-trip pipelines between readable formats.
34+
* **Collapse support**: `[!NOTE]-` (collapsed) and `[!NOTE]+` (expanded) markers are normalized to `collapse="true/false"` and round-trip correctly through all readable formats. Write-only formats use their native collapse syntax (`???` for MkDocs, `dropdown` for MyST, `:::details` for VitePress/Docusaurus).
35+
* **Inline title capture**: `> [!NOTE] My title` extracts the title correctly from the marker line.
36+
* **Any casing accepted**: `[!NOTE]`, `[!Note]`, `[!note]` all normalize identically.
37+
* **Quarto div normalizer**: `:::{.callout-*}` divs are read and converted to the intermediate format, enabling Quarto → any format pipelines.
38+
* **Pandoc 3.9 / Sphinx normalizer**: `:::{.note}` plain classed divs with optional `.title` child or `title=` attribute are recognized.
39+
* **Extended callout type whitelist**: 25 built-in types covering all major ecosystems (Obsidian, MkDocs, MyST, Sphinx, VitePress).
40+
* **`custom-types`** frontmatter option: add types beyond the built-in whitelist without modifying the filter.
41+
* **Quarto source roundtrip**: `quarto-format` → `pandoc-md` → `quarto-format` verified to produce identical output.
42+
* **Test metadata files** updated to new `out-format` nested form.
43+
* **Dispatcher pattern**: one `write_callout` entry point routes to the correct writer via a `Writers` table keyed by container type.
44+
45+
### Changed
46+
47+
* Title handling mimics pandoc 3.9: the `.title` Div child is only inserted when the user explicitly sets a title. No auto-generation from the type name.
48+
* `write_blockquote` produces pure AST objects — `RawInline("markdown", ...)` for the marker prevents Pandoc from escaping `[` as `\[`, and `BlockQuote` wraps the content instead of `RawBlock` markdown text. Enables correct round-trip parsing.
49+
* Format defaults extracted into a `FormatDefaults` table — no more `if/elseif` chain.
50+
* Magic strings replaced by `TypeCase` and `Container` enums with runtime error protection against typos.
51+
52+
## \[0.1.0]
53+
54+
* Converts `> [!WORD]` GitHub alert blockquotes to classed Divs.
55+
* Two output paths: Quarto (`callout-*` class + `title` attribute) and plain Pandoc (bare class + `.title` Div child).
56+
* Auto-detects Quarto via the `quarto` global.
57+
* Accepts uppercase `[!NOTE]` markers only.
58+
* Per-document options via `quarto-format: true` / `pandoc-format: true` flags.

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ authors:
44
- family-names: "Barrio"
55
given-names: "Pedro Luis"
66
title: "alerts-normalize.lua"
7-
version: 0.1.0
7+
version: 1.0.0
88
date-released: 2026-03-22
99
url: "https://github.com/plbarrio/alerts-normalize"
1010
license: GPL-3.0-or-later

Makefile

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
#
33
# Targets:
44
# test — run all tests
5-
# test-pandoc — plain Pandoc path (5 cases)
6-
# test-quarto — forced Quarto path via quarto-format: true (5 cases)
7-
# test-quarto-pandoc — Quarto runner + pandoc-format: true (5 cases, requires quarto)
5+
# test-pandoc — plain Pandoc path (11 cases)
6+
# test-quarto — forced Quarto path via out-format: quarto-format (6 cases)
7+
# test-quarto-pandoc — Quarto runner + out-format: pandoc-format (5 cases, requires quarto)
8+
# test-roundtrip — round-trip through all readable formats via pandoc-md
89

910
FILTER = alerts-normalize.lua
1011
INPUT_DIR = test/input
@@ -15,22 +16,34 @@ META_QUARTO = $(INPUT_DIR)/alert-normalize-quarto-mode.yaml
1516
# strips pandoc-api-version from JSON output before comparing
1617
STRIP_VER = python3 -c "import sys,json; d=json.load(sys.stdin); d.pop('pandoc-api-version',None); d.pop('meta',None); print(json.dumps(d))"
1718

18-
.PHONY: test test-pandoc test-quarto test-quarto-pandoc \
19+
# convert md through a format and back to pandoc-md
20+
STEP = pandoc -f markdown -t markdown --lua-filter=$(FILTER) --metadata alerts-normalize
21+
22+
.PHONY: test test-pandoc test-quarto test-quarto-pandoc test-roundtrip \
1923
test-pandoc-basic test-pandoc-empty test-pandoc-multipara \
2024
test-pandoc-rich test-pandoc-passthrough \
25+
test-pandoc-title test-pandoc-collapse test-pandoc-title-only \
26+
test-pandoc-custom-type test-pandoc-pandoc-md-source \
27+
test-pandoc-quarto-titled \
2128
test-quarto-basic test-quarto-empty test-quarto-multipara \
22-
test-quarto-rich test-quarto-passthrough \
29+
test-quarto-rich test-quarto-passthrough test-quarto-titled \
2330
test-quarto-pandoc-basic test-quarto-pandoc-empty \
2431
test-quarto-pandoc-multipara test-quarto-pandoc-rich \
2532
test-quarto-pandoc-passthrough \
33+
test-roundtrip-quarto test-roundtrip-github \
34+
test-roundtrip-obsidian test-roundtrip-quarto-source \
35+
test-roundtrip-title test-roundtrip-collapse \
2636
generate generate-pandoc generate-quarto
2737

28-
test: test-pandoc test-quarto test-quarto-pandoc
38+
test: test-pandoc test-quarto test-quarto-pandoc test-roundtrip
2939

3040
# --- Plain Pandoc tests ---
3141

3242
test-pandoc: test-pandoc-basic test-pandoc-empty test-pandoc-multipara \
33-
test-pandoc-rich test-pandoc-passthrough
43+
test-pandoc-rich test-pandoc-passthrough \
44+
test-pandoc-title test-pandoc-collapse test-pandoc-title-only \
45+
test-pandoc-custom-type test-pandoc-pandoc-md-source \
46+
test-pandoc-quarto-titled
3447

3548
test-pandoc-basic:
3649
@echo -n "test-pandoc-basic: "
@@ -67,10 +80,52 @@ test-pandoc-passthrough:
6780
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-passthrough-pandoc.json
6881
@echo "OK"
6982

70-
# --- Quarto forced via quarto-format: true ---
83+
test-pandoc-title:
84+
@echo -n "test-pandoc-title: "
85+
@pandoc $(INPUT_DIR)/alert-normalize-title.md \
86+
--lua-filter=$(FILTER) -t json \
87+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-title-pandoc.json
88+
@echo "OK"
89+
90+
test-pandoc-collapse:
91+
@echo -n "test-pandoc-collapse: "
92+
@pandoc $(INPUT_DIR)/alert-normalize-collapse.md \
93+
--lua-filter=$(FILTER) -t json \
94+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-collapse-pandoc.json
95+
@echo "OK"
96+
97+
test-pandoc-title-only:
98+
@echo -n "test-pandoc-title-only: "
99+
@pandoc $(INPUT_DIR)/alert-normalize-title-only.md \
100+
--lua-filter=$(FILTER) -t json \
101+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-title-only-pandoc.json
102+
@echo "OK"
103+
104+
test-pandoc-custom-type:
105+
@echo -n "test-pandoc-custom-type: "
106+
@pandoc $(INPUT_DIR)/alert-normalize-custom-type.md \
107+
--lua-filter=$(FILTER) -t json \
108+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-custom-type-pandoc.json
109+
@echo "OK"
110+
111+
test-pandoc-pandoc-md-source:
112+
@echo -n "test-pandoc-pandoc-md-source: "
113+
@pandoc $(INPUT_DIR)/alert-normalize-pandoc-md-source.md \
114+
--lua-filter=$(FILTER) -t json \
115+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-pandoc-md-source-pandoc.json
116+
@echo "OK"
117+
118+
test-pandoc-quarto-titled:
119+
@echo -n "test-pandoc-quarto-titled: "
120+
@pandoc $(INPUT_DIR)/alert-normalize-quarto-titled.md \
121+
--lua-filter=$(FILTER) -t json \
122+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-quarto-titled-pandoc.json
123+
@echo "OK"
124+
125+
# --- Quarto forced via out-format: quarto-format ---
71126

72127
test-quarto: test-quarto-basic test-quarto-empty test-quarto-multipara \
73-
test-quarto-rich test-quarto-passthrough
128+
test-quarto-rich test-quarto-passthrough test-quarto-titled
74129

75130
test-quarto-basic:
76131
@echo -n "test-quarto-basic: "
@@ -107,7 +162,14 @@ test-quarto-passthrough:
107162
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-passthrough-quarto.json
108163
@echo "OK"
109164

110-
# --- Quarto runner + pandoc-format: true (requires quarto) ---
165+
test-quarto-titled:
166+
@echo -n "test-quarto-titled: "
167+
@pandoc $(INPUT_DIR)/alert-normalize-quarto-titled.md \
168+
--lua-filter=$(FILTER) --metadata-file=$(META_QUARTO) -t json \
169+
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-quarto-titled-quarto.json
170+
@echo "OK"
171+
172+
# --- Quarto runner + out-format: pandoc-format (requires quarto) ---
111173

112174
test-quarto-pandoc: test-quarto-pandoc-basic test-quarto-pandoc-empty \
113175
test-quarto-pandoc-multipara test-quarto-pandoc-rich \
@@ -148,14 +210,99 @@ test-quarto-pandoc-passthrough:
148210
| $(STRIP_VER) | diff - $(EXPECTED)/alert-normalize-passthrough-pandoc.json
149211
@echo "OK"
150212

213+
# --- Round-trip tests via pandoc-md intermediate ---
214+
#
215+
# Readable formats only: github, obsidian, quarto.
216+
# pandoc-format is write-only — no reader, not included in roundtrip.
217+
218+
ROUNDTRIP_INPUTS = $(INPUT_DIR)/alert-normalize.md \
219+
$(INPUT_DIR)/alert-normalize-empty.md \
220+
$(INPUT_DIR)/alert-normalize-multipara.md \
221+
$(INPUT_DIR)/alert-normalize-rich.md
222+
223+
ROUNDTRIP_FORMATS = quarto-format github-format obsidian-format
224+
225+
test-roundtrip: test-roundtrip-quarto test-roundtrip-github \
226+
test-roundtrip-obsidian test-roundtrip-quarto-source \
227+
test-roundtrip-title test-roundtrip-collapse
228+
229+
test-roundtrip-quarto:
230+
@echo -n "test-roundtrip-quarto: "
231+
@for f in $(ROUNDTRIP_INPUTS); do \
232+
pandoc $$f --lua-filter=$(FILTER) --metadata alerts-normalize=pandoc-md \
233+
-t markdown --wrap=none > /tmp/rt-orig.md; \
234+
$(STEP)=quarto-format -t markdown --wrap=none < /tmp/rt-orig.md \
235+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
236+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$f"; exit 1; }; \
237+
done
238+
@echo "OK"
239+
240+
test-roundtrip-github:
241+
@echo -n "test-roundtrip-github: "
242+
@for f in $(ROUNDTRIP_INPUTS); do \
243+
pandoc $$f --lua-filter=$(FILTER) --metadata alerts-normalize=pandoc-md \
244+
-t markdown --wrap=none > /tmp/rt-orig.md; \
245+
$(STEP)=github-format -t markdown --wrap=none < /tmp/rt-orig.md \
246+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
247+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$f"; exit 1; }; \
248+
done
249+
@echo "OK"
250+
251+
test-roundtrip-obsidian:
252+
@echo -n "test-roundtrip-obsidian: "
253+
@for f in $(ROUNDTRIP_INPUTS); do \
254+
pandoc $$f --lua-filter=$(FILTER) --metadata alerts-normalize=pandoc-md \
255+
-t markdown --wrap=none > /tmp/rt-orig.md; \
256+
$(STEP)=obsidian-format -t markdown --wrap=none < /tmp/rt-orig.md \
257+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
258+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$f"; exit 1; }; \
259+
done
260+
@echo "OK"
261+
262+
test-roundtrip-quarto-source:
263+
@echo -n "test-roundtrip-quarto-source: "
264+
@for f in $(INPUT_DIR)/alert-normalize-quarto-titled.md $(ROUNDTRIP_INPUTS); do \
265+
pandoc $$f --lua-filter=$(FILTER) --metadata alerts-normalize=pandoc-md \
266+
-t markdown --wrap=none > /tmp/rt-orig.md; \
267+
$(STEP)=quarto-format -t markdown --wrap=none < /tmp/rt-orig.md \
268+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
269+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$f"; exit 1; }; \
270+
done
271+
@echo "OK"
272+
273+
test-roundtrip-title:
274+
@echo -n "test-roundtrip-title: "
275+
@pandoc $(INPUT_DIR)/alert-normalize-title.md --lua-filter=$(FILTER) \
276+
--metadata alerts-normalize=pandoc-md -t markdown --wrap=none > /tmp/rt-orig.md; \
277+
for fmt in $(ROUNDTRIP_FORMATS); do \
278+
$(STEP)=$$fmt -t markdown --wrap=none < /tmp/rt-orig.md \
279+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
280+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$fmt"; exit 1; }; \
281+
done
282+
@echo "OK"
283+
284+
test-roundtrip-collapse:
285+
@echo -n "test-roundtrip-collapse: "
286+
@pandoc $(INPUT_DIR)/alert-normalize-collapse.md --lua-filter=$(FILTER) \
287+
--metadata alerts-normalize=pandoc-md -t markdown --wrap=none > /tmp/rt-orig.md; \
288+
for fmt in $(ROUNDTRIP_FORMATS); do \
289+
$(STEP)=$$fmt -t markdown --wrap=none < /tmp/rt-orig.md \
290+
| $(STEP)=pandoc-md -t markdown --wrap=none > /tmp/rt-trip.md; \
291+
diff /tmp/rt-orig.md /tmp/rt-trip.md || { echo "FAIL: $$fmt"; exit 1; }; \
292+
done
293+
@echo "OK"
294+
151295
# --- Generate expected files ---
152296

153297
generate: generate-pandoc generate-quarto
154298

155299
generate-pandoc:
156300
@echo "Generating pandoc expected files..."
157301
@for f in alert-normalize alert-normalize-empty alert-normalize-multipara \
158-
alert-normalize-rich alert-normalize-passthrough; do \
302+
alert-normalize-rich alert-normalize-passthrough \
303+
alert-normalize-title alert-normalize-collapse \
304+
alert-normalize-title-only alert-normalize-custom-type \
305+
alert-normalize-pandoc-md-source alert-normalize-quarto-titled; do \
159306
echo -n " $$f: "; \
160307
pandoc $(INPUT_DIR)/$$f.md --lua-filter=$(FILTER) -t json \
161308
| $(STRIP_VER) > $(EXPECTED)/$$f-pandoc.json && echo "OK"; \
@@ -164,7 +311,8 @@ generate-pandoc:
164311
generate-quarto:
165312
@echo "Generating quarto expected files..."
166313
@for f in alert-normalize alert-normalize-empty alert-normalize-multipara \
167-
alert-normalize-rich alert-normalize-passthrough; do \
314+
alert-normalize-rich alert-normalize-passthrough \
315+
alert-normalize-quarto-titled; do \
168316
echo -n " $$f: "; \
169317
pandoc $(INPUT_DIR)/$$f.md --lua-filter=$(FILTER) \
170318
--metadata-file=$(META_QUARTO) -t json \

0 commit comments

Comments
 (0)