granite-mem 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Granite — HTML Markdown Renderer
3
+ *
4
+ * Converts markdown + wikilinks to semantic HTML.
5
+ * Wikilinks become real <a> elements with hover previews.
6
+ */
7
+
8
+ const MarkdownRenderer = (() => {
9
+
10
+ // ── Inline parser — handles [[wikilinks]], **bold**, *italic*, `code`, [links](url) ──
11
+ function parseInline(text, outgoingLinks) {
12
+ const re = /(\[\[([^\]]+)\]\])|(`([^`]+)`)|(\*\*(.+?)\*\*)|(\*(.+?)\*)|(\[([^\]]+)\]\(([^)]+)\))/g;
13
+ let result = '';
14
+ let lastIndex = 0;
15
+ let match;
16
+
17
+ while ((match = re.exec(text)) !== null) {
18
+ if (match.index > lastIndex) {
19
+ result += escapeHtml(text.slice(lastIndex, match.index));
20
+ }
21
+
22
+ if (match[1]) {
23
+ // [[wikilink]] or [[target|display]]
24
+ const inner = match[2];
25
+ const parts = inner.split('|');
26
+ const target = parts[0].trim();
27
+ const display = parts.length > 1 ? parts[1].trim() : target;
28
+ const linkInfo = outgoingLinks?.find(l => l.target === target);
29
+ const resolved = linkInfo?.resolved ?? false;
30
+ const slug = linkInfo?.resolved_slug || null;
31
+
32
+ if (resolved && slug) {
33
+ result += `<a class="wikilink" data-slug="${escapeAttr(slug)}" data-resolved="true" href="javascript:void(0)">${escapeHtml(display)}</a>`;
34
+ } else {
35
+ result += `<span class="wikilink-broken">${escapeHtml(display)}</span>`;
36
+ }
37
+ } else if (match[3]) {
38
+ // `inline code`
39
+ result += `<code class="inline-code">${escapeHtml(match[4])}</code>`;
40
+ } else if (match[5]) {
41
+ // **bold**
42
+ result += `<strong>${escapeHtml(match[6])}</strong>`;
43
+ } else if (match[7]) {
44
+ // *italic*
45
+ result += `<em>${escapeHtml(match[8])}</em>`;
46
+ } else if (match[9]) {
47
+ // [text](url)
48
+ result += `<a class="external-link" href="${escapeAttr(match[11])}" target="_blank" rel="noopener">${escapeHtml(match[10])}</a>`;
49
+ }
50
+
51
+ lastIndex = match.index + match[0].length;
52
+ }
53
+
54
+ if (lastIndex < text.length) {
55
+ result += escapeHtml(text.slice(lastIndex));
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ // ── Block-level markdown parser ──
62
+ function render(body, outgoingLinks) {
63
+ const lines = body.split('\n');
64
+ const blocks = [];
65
+ let inCodeBlock = false;
66
+ let codeBuffer = [];
67
+ let codeLang = '';
68
+ let inList = false;
69
+ let listType = null; // 'ul' or 'ol'
70
+ let listItems = [];
71
+
72
+ function flushList() {
73
+ if (listItems.length > 0) {
74
+ const tag = listType === 'ol' ? 'ol' : 'ul';
75
+ blocks.push(`<${tag} class="md-list">${listItems.join('')}</${tag}>`);
76
+ listItems = [];
77
+ inList = false;
78
+ listType = null;
79
+ }
80
+ }
81
+
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i];
84
+
85
+ // Code block toggle
86
+ if (line.startsWith('```')) {
87
+ if (inCodeBlock) {
88
+ const langClass = codeLang ? ` data-lang="${escapeAttr(codeLang)}"` : '';
89
+ const langLabel = codeLang ? `<span class="code-lang">${escapeHtml(codeLang)}</span>` : '';
90
+ blocks.push(`<div class="code-block"${langClass}>${langLabel}<pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
91
+ codeBuffer = [];
92
+ inCodeBlock = false;
93
+ } else {
94
+ flushList();
95
+ inCodeBlock = true;
96
+ codeLang = line.slice(3).trim();
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (inCodeBlock) {
102
+ codeBuffer.push(line);
103
+ continue;
104
+ }
105
+
106
+ // Blank line
107
+ if (line.trim() === '') {
108
+ flushList();
109
+ continue;
110
+ }
111
+
112
+ // Headings
113
+ const hMatch = line.match(/^(#{1,3})\s+(.+)/);
114
+ if (hMatch) {
115
+ flushList();
116
+ const level = hMatch[1].length;
117
+ const content = parseInline(hMatch[2], outgoingLinks);
118
+ blocks.push(`<h${level} class="md-h${level}">${content}</h${level}>`);
119
+ continue;
120
+ }
121
+
122
+ // Horizontal rule
123
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
124
+ flushList();
125
+ blocks.push('<hr class="md-hr">');
126
+ continue;
127
+ }
128
+
129
+ // Blockquote
130
+ if (line.startsWith('>')) {
131
+ flushList();
132
+ const text = line.replace(/^>\s?/, '');
133
+ const content = parseInline(text, outgoingLinks);
134
+ blocks.push(`<blockquote class="md-blockquote">${content}</blockquote>`);
135
+ continue;
136
+ }
137
+
138
+ // Unordered list
139
+ const bulletMatch = line.match(/^(\s*)[*\-+]\s+(.+)/);
140
+ if (bulletMatch) {
141
+ if (listType !== 'ul') flushList();
142
+ inList = true;
143
+ listType = 'ul';
144
+ const content = parseInline(bulletMatch[2], outgoingLinks);
145
+ listItems.push(`<li>${content}</li>`);
146
+ continue;
147
+ }
148
+
149
+ // Ordered list
150
+ const numMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
151
+ if (numMatch) {
152
+ if (listType !== 'ol') flushList();
153
+ inList = true;
154
+ listType = 'ol';
155
+ const content = parseInline(numMatch[2], outgoingLinks);
156
+ listItems.push(`<li>${content}</li>`);
157
+ continue;
158
+ }
159
+
160
+ // Body paragraph
161
+ flushList();
162
+ const content = parseInline(line, outgoingLinks);
163
+ blocks.push(`<p class="md-body">${content}</p>`);
164
+ }
165
+
166
+ // Flush remaining
167
+ flushList();
168
+ if (inCodeBlock && codeBuffer.length > 0) {
169
+ blocks.push(`<div class="code-block"><pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
170
+ }
171
+
172
+ return blocks.join('\n');
173
+ }
174
+
175
+ // ── Helpers ──
176
+ function escapeHtml(str) {
177
+ return str
178
+ .replace(/&/g, '&amp;')
179
+ .replace(/</g, '&lt;')
180
+ .replace(/>/g, '&gt;')
181
+ .replace(/"/g, '&quot;');
182
+ }
183
+
184
+ function escapeAttr(str) {
185
+ return str.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
186
+ }
187
+
188
+ return { render, parseInline, escapeHtml };
189
+ })();