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
04cb1d84a03dc794df3c596927b855416d357a9cParent: 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-cospan2 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+}