refactor: compose two serialized lenses for markdown rendering Rendering pipeline uses two serialized lens files: 1. marked-to-relationaltext.lens.json (token type → RT feature) 2. relationaltext-to-html.lens.json (RT feature → HTML element) Composed at module init. No hand-written mappings.

Author: Aaron Steven White
Commit 04cb1d84a03dc794df3c596927b855416d357a9c
Parent: bee63c3797
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
2 files changed +59 -39
@@ -1,45 +1,39 @@
11 <script lang="ts" module>
2-	// Load the relationaltext → HTML lens rules at module level.
3-	// This is a panproto lens that maps feature names to HTML element names.
4-	import lensRules from '$lib/data/relationaltext-to-html.lens.json';
2+	// Two serialized lenses compose the rendering pipeline:
3+	//   marked token → relationaltext feature → HTML element
4+	import markedToRt from '$lib/data/marked-to-relationaltext.lens.json';
5+	import rtToHtml from '$lib/data/relationaltext-to-html.lens.json';
56 
6-	type LensRule = { match: { name: string }; replace: { name: string | { template: string }; renameAttrs?: Record<string, string>; dropAttrs?: string[] } | null };
7+	type LensRule = {
8+		match: { name: string };
9+		replace: { name: string | { template: string }; renameAttrs?: Record<string, string>; dropAttrs?: string[] } | null;
10+	};
711 
8-	// Build a lookup: feature name → HTML element name + attr transforms
9-	const featureToElement: Record<string, { tag: string; renameAttrs?: Record<string, string>; dropAttrs?: string[] }> = {};
10-	for (const rule of (lensRules as { rules: LensRule[] }).rules) {
12+	// Build composed lookup: marked token type → HTML element
13+	// This is the composition of the two lenses: markedToRt ∘ rtToHtml
14+	const rtToElement: Record<string, { tag: string; renameAttrs?: Record<string, string> }> = {};
15+	for (const rule of (rtToHtml as { rules: LensRule[] }).rules) {
1116 		if (!rule.replace) continue;
1217 		const tag = typeof rule.replace.name === 'string'
1318 			? rule.replace.name
14-			: rule.replace.name.template; // e.g., "h{level}"
15-		featureToElement[rule.match.name] = {
16-			tag,
17-			renameAttrs: rule.replace.renameAttrs,
18-			dropAttrs: rule.replace.dropAttrs,
19-		};
19+			: rule.replace.name.template;
20+		rtToElement[rule.match.name] = { tag, renameAttrs: rule.replace.renameAttrs };
2021 	}
2122 
22-	// Map marked.js token types to relationaltext feature names
23-	const tokenToFeature: Record<string, string> = {
24-		paragraph: 'paragraph',
25-		heading: 'heading',
26-		code: 'code-block',
27-		blockquote: 'blockquote',
28-		hr: 'horizontal-rule',
29-		strong: 'bold',
30-		em: 'italic',
31-		codespan: 'code',
32-		del: 'strikethrough',
33-		link: 'link',
34-		image: 'image',
35-		br: 'line-break',
36-	};
23+	const tokenToRt: Record<string, string | null> = {};
24+	for (const rule of (markedToRt as { rules: LensRule[] }).rules) {
25+		tokenToRt[rule.match.name] = rule.replace
26+			? (typeof rule.replace.name === 'string' ? rule.replace.name : rule.replace.name.template)
27+			: null;
28+	}
3729 
38-	function resolveTag(featureName: string, attrs?: Record<string, unknown>): string {
39-		const mapping = featureToElement[featureName];
30+	// Composed lens: token type → HTML tag
31+	function resolveTag(tokenType: string, attrs?: Record<string, unknown>): string | null {
32+		const rtFeature = tokenToRt[tokenType];
33+		if (rtFeature === null || rtFeature === undefined) return null;
34+		const mapping = rtToElement[rtFeature];
4035 		if (!mapping) return 'span';
4136 		if (mapping.tag.includes('{')) {
42-			// Template: "h{level}" → "h1", "h2", etc.
4337 			return mapping.tag.replace(/\{(\w+)\}/g, (_, key) => String(attrs?.[key] ?? ''));
4438 		}
4539 		return mapping.tag;
@@ -65,9 +59,10 @@
6559 {/if}
6660 
6761 {#snippet blockToken(token: marked.Token)}
68-	{@const feature = tokenToFeature[token.type]}
69-	{@const tag = feature ? resolveTag(feature, 'depth' in token ? { level: token.depth } : {}) : ''}
70-	{#if token.type === 'paragraph'}
62+	{@const tag = resolveTag(token.type, 'depth' in token ? { level: token.depth } : {})}
63+	{#if tag === null}
64+		<!-- Lens maps to null: skip -->
65+	{:else if token.type === 'paragraph'}
7166 		<p>{@render inlineTokens(token.tokens ?? [])}</p>
7267 	{:else if token.type === 'heading'}
7368 		<svelte:element this={tag}>{@render inlineTokens(token.tokens ?? [])}</svelte:element>
@@ -95,8 +90,6 @@
9590 		</svelte:element>
9691 	{:else if token.type === 'hr'}
9792 		<hr />
98-	{:else if token.type === 'space'}
99-		<!-- skip -->
10093 	{:else if token.type === 'text'}
10194 		{#if 'tokens' in token && token.tokens}
10295 			<p>{@render inlineTokens(token.tokens)}</p>
@@ -113,9 +106,10 @@
113106 {/snippet}
114107 
115108 {#snippet inlineToken(token: marked.Token)}
116-	{@const feature = tokenToFeature[token.type]}
117-	{@const tag = feature ? resolveTag(feature) : ''}
118-	{#if token.type === 'text'}
109+	{@const tag = resolveTag(token.type)}
110+	{#if tag === null}
111+		<!-- skip -->
112+	{:else if token.type === 'text'}
119113 		{token.text}
120114 	{:else if token.type === 'strong'}
121115 		<strong>{@render inlineTokens(token.tokens ?? [])}</strong>
@@ -0,0 +1,26 @@
1+{
2+  "$type": "org.relationaltext.lens",
3+  "id": "cospan.marked.to.relationaltext.v1",
4+  "description": "Map marked.js token types to relationaltext feature names",
5+  "source": "cospan.marked.token",
6+  "target": "org.relationaltext.facet",
7+  "invertible": false,
8+  "rules": [
9+    { "match": { "name": "paragraph" }, "replace": { "name": "paragraph" } },
10+    { "match": { "name": "heading" }, "replace": { "name": "heading" } },
11+    { "match": { "name": "code" }, "replace": { "name": "code-block" } },
12+    { "match": { "name": "blockquote" }, "replace": { "name": "blockquote" } },
13+    { "match": { "name": "hr" }, "replace": { "name": "horizontal-rule" } },
14+    { "match": { "name": "strong" }, "replace": { "name": "bold" } },
15+    { "match": { "name": "em" }, "replace": { "name": "italic" } },
16+    { "match": { "name": "codespan" }, "replace": { "name": "code" } },
17+    { "match": { "name": "del" }, "replace": { "name": "strikethrough" } },
18+    { "match": { "name": "link" }, "replace": { "name": "link" } },
19+    { "match": { "name": "image" }, "replace": { "name": "image" } },
20+    { "match": { "name": "br" }, "replace": { "name": "line-break" } },
21+    { "match": { "name": "list" }, "replace": { "name": "list" } },
22+    { "match": { "name": "text" }, "replace": { "name": "text" } },
23+    { "match": { "name": "space" }, "replace": null },
24+    { "match": { "name": "escape" }, "replace": { "name": "text" } }
25+  ]
26+}
cospan · schematic version control on atproto built on AT Protocol