granite-mem 0.1.7 → 0.1.8
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.
- package/README.md +34 -14
- package/dist/index.js +1002 -910
- package/dist/public/app.js +252 -300
- package/dist/public/graph.js +11 -14
- package/dist/public/index.html +115 -102
- package/dist/public/markdown-renderer.js +49 -51
- package/dist/public/style.css +595 -558
- package/package.json +2 -1
package/dist/public/graph.js
CHANGED
|
@@ -10,23 +10,20 @@ const GraphEngine = (() => {
|
|
|
10
10
|
|
|
11
11
|
// ── Type colors ──
|
|
12
12
|
const TYPE_COLORS = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
project: { fill: '#c4b5fd', glow: 'rgba(196, 181, 253, 0.3)' },
|
|
19
|
-
decision: { fill: '#fca5a5', glow: 'rgba(252, 165, 165, 0.3)' },
|
|
20
|
-
_default: { fill: '#6e6e7a', glow: 'rgba(110, 110, 122, 0.3)' },
|
|
13
|
+
note: { fill: '#a8aaff', glow: 'rgba(168, 170, 255, 0.25)' },
|
|
14
|
+
source: { fill: '#5cd9a8', glow: 'rgba(92, 217, 168, 0.25)' },
|
|
15
|
+
synthesis: { fill: '#e0a0ff', glow: 'rgba(224, 160, 255, 0.25)' },
|
|
16
|
+
output: { fill: '#ffb86c', glow: 'rgba(255, 184, 108, 0.25)' },
|
|
17
|
+
_default: { fill: '#6b6b7a', glow: 'rgba(107, 107, 122, 0.25)' },
|
|
21
18
|
};
|
|
22
19
|
|
|
23
|
-
const BG_COLOR = '#
|
|
24
|
-
const EDGE_COLOR = 'rgba(
|
|
25
|
-
const EDGE_HIGHLIGHT = 'rgba(
|
|
26
|
-
const LABEL_COLOR = '#
|
|
27
|
-
const LABEL_DIM = 'rgba(
|
|
20
|
+
const BG_COLOR = '#050507';
|
|
21
|
+
const EDGE_COLOR = 'rgba(60, 60, 75, 0.3)';
|
|
22
|
+
const EDGE_HIGHLIGHT = 'rgba(168, 170, 255, 0.5)';
|
|
23
|
+
const LABEL_COLOR = '#a8a8b4';
|
|
24
|
+
const LABEL_DIM = 'rgba(107, 107, 122, 0.25)';
|
|
28
25
|
const LABEL_FONT = '11px "Instrument Sans", sans-serif';
|
|
29
|
-
const NODE_ACTIVE_RING = '#
|
|
26
|
+
const NODE_ACTIVE_RING = '#7c7fff';
|
|
30
27
|
|
|
31
28
|
// ── State ──
|
|
32
29
|
let canvas, ctx;
|
package/dist/public/index.html
CHANGED
|
@@ -11,145 +11,158 @@
|
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="app">
|
|
14
|
-
<!--
|
|
15
|
-
<
|
|
16
|
-
<div id="
|
|
17
|
-
<
|
|
18
|
-
<svg width="
|
|
19
|
-
<
|
|
20
|
-
<rect x="11" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
|
|
21
|
-
<rect x="1" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
|
|
22
|
-
<rect x="11" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.3"/>
|
|
14
|
+
<!-- Command bar overlay -->
|
|
15
|
+
<div id="command-bar-overlay" class="overlay hidden" role="dialog" aria-label="Search" aria-modal="true">
|
|
16
|
+
<div id="command-bar">
|
|
17
|
+
<div class="command-input-wrap">
|
|
18
|
+
<svg class="command-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
19
|
+
<circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/>
|
|
23
20
|
</svg>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
<input type="text" id="command-input" placeholder="Search notes..." autocomplete="off" spellcheck="false" aria-label="Search notes" role="combobox" aria-expanded="false" aria-controls="command-results" aria-autocomplete="list">
|
|
22
|
+
<kbd class="command-kbd">esc</kbd>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="command-results" role="listbox" aria-label="Search results" aria-live="polite"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Create note overlay -->
|
|
29
|
+
<div id="create-overlay" class="overlay hidden" role="dialog" aria-label="Create note" aria-modal="true">
|
|
30
|
+
<div id="create-dialog">
|
|
31
|
+
<div class="create-header">
|
|
32
|
+
<span>New note</span>
|
|
33
|
+
<button id="create-close" class="icon-btn" title="Close">
|
|
34
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="create-fields">
|
|
38
|
+
<div class="field-row">
|
|
39
|
+
<label for="create-type">Type</label>
|
|
40
|
+
<select id="create-type"></select>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="field-row">
|
|
43
|
+
<label for="create-title">Title</label>
|
|
44
|
+
<input type="text" id="create-title" placeholder="Note title" autocomplete="off" spellcheck="false" required maxlength="200">
|
|
45
|
+
</div>
|
|
46
|
+
<div class="field-row field-grow">
|
|
47
|
+
<label for="create-body">Content</label>
|
|
48
|
+
<textarea id="create-body" placeholder="Start writing..." spellcheck="false"></textarea>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<button id="create-submit">Create</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Mobile header -->
|
|
56
|
+
<header id="mobile-header">
|
|
57
|
+
<button id="mobile-menu" class="icon-btn" aria-label="Open navigation">
|
|
58
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 5h12M3 9h12M3 13h12"/></svg>
|
|
59
|
+
</button>
|
|
60
|
+
<span class="mobile-brand">Granite</span>
|
|
61
|
+
<button id="mobile-search" class="icon-btn" aria-label="Search">
|
|
62
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg>
|
|
63
|
+
</button>
|
|
64
|
+
</header>
|
|
65
|
+
|
|
66
|
+
<!-- Nav backdrop (mobile) -->
|
|
67
|
+
<div id="nav-backdrop" class="hidden"></div>
|
|
68
|
+
|
|
69
|
+
<!-- Navigation -->
|
|
70
|
+
<nav id="nav">
|
|
71
|
+
<div class="nav-brand">
|
|
72
|
+
<svg width="16" height="16" viewBox="0 0 18 18" fill="none">
|
|
73
|
+
<rect x="1" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.9"/>
|
|
74
|
+
<rect x="11" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.45"/>
|
|
75
|
+
<rect x="1" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.45"/>
|
|
76
|
+
<rect x="11" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.2"/>
|
|
77
|
+
</svg>
|
|
78
|
+
<span>Granite</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="nav-actions">
|
|
81
|
+
<button id="nav-search" class="nav-btn" title="Search (⌘K)">
|
|
82
|
+
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
83
|
+
<circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/>
|
|
37
84
|
</svg>
|
|
85
|
+
<span>Search</span>
|
|
86
|
+
<kbd>⌘K</kbd>
|
|
38
87
|
</button>
|
|
39
|
-
<button id="
|
|
40
|
-
<svg width="
|
|
88
|
+
<button id="nav-new" class="nav-btn" title="New note (⌘N)">
|
|
89
|
+
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
41
90
|
<path d="M8 3v10M3 8h10"/>
|
|
42
91
|
</svg>
|
|
92
|
+
<span>New</span>
|
|
43
93
|
</button>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<path d="M5.5 2v12"/>
|
|
94
|
+
<button id="nav-graph" class="nav-btn" title="Graph (⌘G)">
|
|
95
|
+
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
96
|
+
<circle cx="4" cy="4" r="2"/><circle cx="12" cy="4" r="2"/><circle cx="8" cy="13" r="2"/>
|
|
97
|
+
<path d="M5.5 5.5L7 11M10.5 5.5L9 11M6 4h4"/>
|
|
49
98
|
</svg>
|
|
99
|
+
<span>Graph</span>
|
|
50
100
|
</button>
|
|
51
101
|
</div>
|
|
52
|
-
<div id="
|
|
53
|
-
<!-- Search -->
|
|
54
|
-
<div id="sidebar-search">
|
|
55
|
-
<div id="search-wrapper">
|
|
56
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.4">
|
|
57
|
-
<circle cx="7" cy="7" r="4.5"/>
|
|
58
|
-
<path d="M10.5 10.5L14 14"/>
|
|
59
|
-
</svg>
|
|
60
|
-
<input type="text" id="search-box" placeholder="Search…" autocomplete="off" spellcheck="false">
|
|
61
|
-
<kbd>⌘K</kbd>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
102
|
+
<div id="nav-note-list">
|
|
64
103
|
<div id="type-filters">
|
|
65
|
-
<button class="type-
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<!-- Note list (default view) -->
|
|
69
|
-
<div id="note-list"></div>
|
|
70
|
-
|
|
71
|
-
<!-- Creation panel (hidden by default) -->
|
|
72
|
-
<div id="create-panel" style="display:none">
|
|
73
|
-
<div id="create-panel-header">
|
|
74
|
-
<span class="create-panel-title">New note</span>
|
|
75
|
-
<button id="create-panel-close" class="create-panel-close" title="Cancel">
|
|
76
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
77
|
-
<path d="M4 4l8 8M12 4l-8 8"/>
|
|
78
|
-
</svg>
|
|
79
|
-
</button>
|
|
80
|
-
</div>
|
|
81
|
-
<div id="create-form">
|
|
82
|
-
<label class="create-label">Type</label>
|
|
83
|
-
<select id="create-type" class="create-select"></select>
|
|
84
|
-
<label class="create-label">Title</label>
|
|
85
|
-
<input type="text" id="create-title" class="create-input" placeholder="Note title…" autocomplete="off" spellcheck="false">
|
|
86
|
-
<label class="create-label">Content</label>
|
|
87
|
-
<textarea id="create-body" class="create-textarea" placeholder="Start writing… (markdown supported)" spellcheck="false"></textarea>
|
|
88
|
-
<button id="create-submit" class="create-submit">Create note</button>
|
|
89
|
-
</div>
|
|
104
|
+
<button class="type-pill active" data-type="">All</button>
|
|
90
105
|
</div>
|
|
106
|
+
<div id="note-list" role="list" aria-label="Notes"></div>
|
|
91
107
|
</div>
|
|
92
|
-
</
|
|
108
|
+
</nav>
|
|
93
109
|
|
|
94
110
|
<!-- Main content -->
|
|
95
111
|
<main id="main">
|
|
96
112
|
<!-- Note view -->
|
|
97
|
-
<div id="
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<path d="M10 2L6 14M4 5L1 8l3 3M12 5l3 3-3 3"/>
|
|
116
|
-
</svg>
|
|
117
|
-
<span id="backlinks-count">0</span> references
|
|
118
|
-
</div>
|
|
119
|
-
<div id="backlinks-list"></div>
|
|
120
|
-
</section>
|
|
113
|
+
<div id="note-view">
|
|
114
|
+
<div id="note-scroll">
|
|
115
|
+
<article id="note-article">
|
|
116
|
+
<div id="note-type-line"></div>
|
|
117
|
+
<h1 id="note-title"></h1>
|
|
118
|
+
<div id="note-meta">
|
|
119
|
+
<span id="note-date"></span>
|
|
120
|
+
<div id="note-tags"></div>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="note-body"></div>
|
|
123
|
+
<section id="backlinks">
|
|
124
|
+
<div class="backlinks-label">
|
|
125
|
+
<span id="backlinks-count">0</span> references
|
|
126
|
+
</div>
|
|
127
|
+
<div id="backlinks-list"></div>
|
|
128
|
+
</section>
|
|
129
|
+
</article>
|
|
130
|
+
</div>
|
|
121
131
|
</div>
|
|
122
132
|
|
|
123
133
|
<!-- Graph view -->
|
|
124
|
-
<div id="graph-view"
|
|
125
|
-
<canvas id="graph-canvas"></canvas>
|
|
134
|
+
<div id="graph-view" class="hidden">
|
|
135
|
+
<canvas id="graph-canvas" aria-label="Knowledge graph visualization"></canvas>
|
|
126
136
|
<div id="graph-tooltip"></div>
|
|
127
137
|
</div>
|
|
128
138
|
|
|
129
139
|
<!-- Empty state -->
|
|
130
140
|
<div id="empty-state">
|
|
131
|
-
<div
|
|
132
|
-
<svg width="
|
|
133
|
-
<circle cx="
|
|
134
|
-
<circle cx="
|
|
135
|
-
<circle cx="
|
|
136
|
-
<path d="
|
|
141
|
+
<div class="empty-glyph">
|
|
142
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
143
|
+
<circle cx="14" cy="14" r="3" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
144
|
+
<circle cx="34" cy="14" r="3" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
145
|
+
<circle cx="24" cy="36" r="3" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
146
|
+
<path d="M16.5 16.5L22 32M31.5 16.5L26 32M18 14h12" stroke="currentColor" stroke-width="0.75" opacity="0.12" stroke-dasharray="3 4"/>
|
|
137
147
|
</svg>
|
|
138
148
|
</div>
|
|
139
|
-
<p>Select a note to begin</p>
|
|
140
|
-
<p class="empty-
|
|
149
|
+
<p class="empty-title">Select a note to begin</p>
|
|
150
|
+
<p class="empty-shortcuts"><kbd>⌘K</kbd> search <span class="dot"></span> <kbd>⌘N</kbd> new <span class="dot"></span> <kbd>⌘G</kbd> graph</p>
|
|
141
151
|
</div>
|
|
142
152
|
</main>
|
|
143
153
|
</div>
|
|
144
154
|
|
|
145
155
|
<!-- Wikilink preview popup -->
|
|
146
|
-
<div id="wikilink-preview"
|
|
156
|
+
<div id="wikilink-preview">
|
|
147
157
|
<div class="preview-type"></div>
|
|
148
158
|
<div class="preview-title"></div>
|
|
149
159
|
<div class="preview-snippet"></div>
|
|
150
160
|
<div class="preview-tags"></div>
|
|
151
161
|
</div>
|
|
152
162
|
|
|
163
|
+
<!-- Grain overlay -->
|
|
164
|
+
<canvas id="grain" aria-hidden="true"></canvas>
|
|
165
|
+
|
|
153
166
|
<script src="/markdown-renderer.js"></script>
|
|
154
167
|
<script src="/graph.js"></script>
|
|
155
168
|
<script src="/app.js"></script>
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
* Granite — HTML Markdown Renderer
|
|
3
3
|
*
|
|
4
4
|
* Converts markdown + wikilinks to semantic HTML.
|
|
5
|
-
*
|
|
5
|
+
* Supports: headings, lists, blockquotes, code, tables, inline formatting, wikilinks.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
7
|
const MarkdownRenderer = (() => {
|
|
9
8
|
|
|
10
|
-
// ── Inline parser — handles [[wikilinks]], **bold**, *italic*, `code`, [links](url) ──
|
|
11
9
|
function parseInline(text, outgoingLinks) {
|
|
12
10
|
const re = /(\[\[([^\]]+)\]\])|(`([^`]+)`)|(\*\*(.+?)\*\*)|(\*(.+?)\*)|(\[([^\]]+)\]\(([^)]+)\))/g;
|
|
13
11
|
let result = '';
|
|
@@ -15,12 +13,9 @@ const MarkdownRenderer = (() => {
|
|
|
15
13
|
let match;
|
|
16
14
|
|
|
17
15
|
while ((match = re.exec(text)) !== null) {
|
|
18
|
-
if (match.index > lastIndex)
|
|
19
|
-
result += escapeHtml(text.slice(lastIndex, match.index));
|
|
20
|
-
}
|
|
16
|
+
if (match.index > lastIndex) result += escapeHtml(text.slice(lastIndex, match.index));
|
|
21
17
|
|
|
22
18
|
if (match[1]) {
|
|
23
|
-
// [[wikilink]] or [[target|display]]
|
|
24
19
|
const inner = match[2];
|
|
25
20
|
const parts = inner.split('|');
|
|
26
21
|
const target = parts[0].trim();
|
|
@@ -28,37 +23,26 @@ const MarkdownRenderer = (() => {
|
|
|
28
23
|
const linkInfo = outgoingLinks?.find(l => l.target === target);
|
|
29
24
|
const resolved = linkInfo?.resolved ?? false;
|
|
30
25
|
const slug = linkInfo?.resolved_slug || null;
|
|
31
|
-
|
|
32
26
|
if (resolved && slug) {
|
|
33
|
-
result += `<a class="wikilink" data-slug="${escapeAttr(slug)}"
|
|
27
|
+
result += `<a class="wikilink" data-slug="${escapeAttr(slug)}" href="#" role="link">${escapeHtml(display)}</a>`;
|
|
34
28
|
} else {
|
|
35
29
|
result += `<span class="wikilink-broken">${escapeHtml(display)}</span>`;
|
|
36
30
|
}
|
|
37
31
|
} else if (match[3]) {
|
|
38
|
-
// `inline code`
|
|
39
32
|
result += `<code class="inline-code">${escapeHtml(match[4])}</code>`;
|
|
40
33
|
} else if (match[5]) {
|
|
41
|
-
// **bold**
|
|
42
34
|
result += `<strong>${escapeHtml(match[6])}</strong>`;
|
|
43
35
|
} else if (match[7]) {
|
|
44
|
-
// *italic*
|
|
45
36
|
result += `<em>${escapeHtml(match[8])}</em>`;
|
|
46
37
|
} else if (match[9]) {
|
|
47
|
-
// [text](url)
|
|
48
38
|
result += `<a class="external-link" href="${escapeAttr(match[11])}" target="_blank" rel="noopener">${escapeHtml(match[10])}</a>`;
|
|
49
39
|
}
|
|
50
|
-
|
|
51
40
|
lastIndex = match.index + match[0].length;
|
|
52
41
|
}
|
|
53
|
-
|
|
54
|
-
if (lastIndex < text.length) {
|
|
55
|
-
result += escapeHtml(text.slice(lastIndex));
|
|
56
|
-
}
|
|
57
|
-
|
|
42
|
+
if (lastIndex < text.length) result += escapeHtml(text.slice(lastIndex));
|
|
58
43
|
return result;
|
|
59
44
|
}
|
|
60
45
|
|
|
61
|
-
// ── Block-level markdown parser ──
|
|
62
46
|
function render(body, outgoingLinks) {
|
|
63
47
|
const lines = body.split('\n');
|
|
64
48
|
const blocks = [];
|
|
@@ -66,7 +50,7 @@ const MarkdownRenderer = (() => {
|
|
|
66
50
|
let codeBuffer = [];
|
|
67
51
|
let codeLang = '';
|
|
68
52
|
let inList = false;
|
|
69
|
-
let listType = null;
|
|
53
|
+
let listType = null;
|
|
70
54
|
let listItems = [];
|
|
71
55
|
|
|
72
56
|
function flushList() {
|
|
@@ -82,12 +66,11 @@ const MarkdownRenderer = (() => {
|
|
|
82
66
|
for (let i = 0; i < lines.length; i++) {
|
|
83
67
|
const line = lines[i];
|
|
84
68
|
|
|
85
|
-
// Code block
|
|
69
|
+
// Code block
|
|
86
70
|
if (line.startsWith('```')) {
|
|
87
71
|
if (inCodeBlock) {
|
|
88
|
-
const langClass = codeLang ? ` data-lang="${escapeAttr(codeLang)}"` : '';
|
|
89
72
|
const langLabel = codeLang ? `<span class="code-lang">${escapeHtml(codeLang)}</span>` : '';
|
|
90
|
-
blocks.push(`<div class="code-block"
|
|
73
|
+
blocks.push(`<div class="code-block">${langLabel}<pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
|
|
91
74
|
codeBuffer = [];
|
|
92
75
|
inCodeBlock = false;
|
|
93
76
|
} else {
|
|
@@ -97,16 +80,25 @@ const MarkdownRenderer = (() => {
|
|
|
97
80
|
}
|
|
98
81
|
continue;
|
|
99
82
|
}
|
|
100
|
-
|
|
101
|
-
if (inCodeBlock) {
|
|
102
|
-
codeBuffer.push(line);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
83
|
+
if (inCodeBlock) { codeBuffer.push(line); continue; }
|
|
105
84
|
|
|
106
85
|
// Blank line
|
|
107
|
-
if (line.trim() === '') {
|
|
86
|
+
if (line.trim() === '') { flushList(); continue; }
|
|
87
|
+
|
|
88
|
+
// Table detection: line starts with |
|
|
89
|
+
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
|
108
90
|
flushList();
|
|
109
|
-
|
|
91
|
+
const tableLines = [];
|
|
92
|
+
let j = i;
|
|
93
|
+
while (j < lines.length && lines[j].trim().startsWith('|') && lines[j].trim().endsWith('|')) {
|
|
94
|
+
tableLines.push(lines[j]);
|
|
95
|
+
j++;
|
|
96
|
+
}
|
|
97
|
+
if (tableLines.length >= 2) {
|
|
98
|
+
blocks.push(renderTable(tableLines, outgoingLinks));
|
|
99
|
+
i = j - 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
// Headings
|
|
@@ -114,12 +106,11 @@ const MarkdownRenderer = (() => {
|
|
|
114
106
|
if (hMatch) {
|
|
115
107
|
flushList();
|
|
116
108
|
const level = hMatch[1].length;
|
|
117
|
-
|
|
118
|
-
blocks.push(`<h${level} class="md-h${level}">${content}</h${level}>`);
|
|
109
|
+
blocks.push(`<h${level} class="md-h${level}">${parseInline(hMatch[2], outgoingLinks)}</h${level}>`);
|
|
119
110
|
continue;
|
|
120
111
|
}
|
|
121
112
|
|
|
122
|
-
//
|
|
113
|
+
// HR
|
|
123
114
|
if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
|
|
124
115
|
flushList();
|
|
125
116
|
blocks.push('<hr class="md-hr">');
|
|
@@ -130,8 +121,7 @@ const MarkdownRenderer = (() => {
|
|
|
130
121
|
if (line.startsWith('>')) {
|
|
131
122
|
flushList();
|
|
132
123
|
const text = line.replace(/^>\s?/, '');
|
|
133
|
-
|
|
134
|
-
blocks.push(`<blockquote class="md-blockquote">${content}</blockquote>`);
|
|
124
|
+
blocks.push(`<blockquote class="md-blockquote">${parseInline(text, outgoingLinks)}</blockquote>`);
|
|
135
125
|
continue;
|
|
136
126
|
}
|
|
137
127
|
|
|
@@ -141,8 +131,7 @@ const MarkdownRenderer = (() => {
|
|
|
141
131
|
if (listType !== 'ul') flushList();
|
|
142
132
|
inList = true;
|
|
143
133
|
listType = 'ul';
|
|
144
|
-
|
|
145
|
-
listItems.push(`<li>${content}</li>`);
|
|
134
|
+
listItems.push(`<li>${parseInline(bulletMatch[2], outgoingLinks)}</li>`);
|
|
146
135
|
continue;
|
|
147
136
|
}
|
|
148
137
|
|
|
@@ -152,33 +141,42 @@ const MarkdownRenderer = (() => {
|
|
|
152
141
|
if (listType !== 'ol') flushList();
|
|
153
142
|
inList = true;
|
|
154
143
|
listType = 'ol';
|
|
155
|
-
|
|
156
|
-
listItems.push(`<li>${content}</li>`);
|
|
144
|
+
listItems.push(`<li>${parseInline(numMatch[2], outgoingLinks)}</li>`);
|
|
157
145
|
continue;
|
|
158
146
|
}
|
|
159
147
|
|
|
160
|
-
//
|
|
148
|
+
// Paragraph
|
|
161
149
|
flushList();
|
|
162
|
-
|
|
163
|
-
blocks.push(`<p class="md-body">${content}</p>`);
|
|
150
|
+
blocks.push(`<p class="md-body">${parseInline(line, outgoingLinks)}</p>`);
|
|
164
151
|
}
|
|
165
152
|
|
|
166
|
-
// Flush remaining
|
|
167
153
|
flushList();
|
|
168
154
|
if (inCodeBlock && codeBuffer.length > 0) {
|
|
169
155
|
blocks.push(`<div class="code-block"><pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
|
|
170
156
|
}
|
|
171
|
-
|
|
172
157
|
return blocks.join('\n');
|
|
173
158
|
}
|
|
174
159
|
|
|
175
|
-
|
|
160
|
+
function renderTable(tableLines, outgoingLinks) {
|
|
161
|
+
const parseRow = (line) => line.trim().replace(/^\||\|$/g, '').split('|').map(cell => cell.trim());
|
|
162
|
+
const headers = parseRow(tableLines[0]);
|
|
163
|
+
// Skip separator row (index 1)
|
|
164
|
+
const rows = tableLines.slice(2).map(parseRow);
|
|
165
|
+
|
|
166
|
+
let html = '<div class="md-table-wrap"><table class="md-table"><thead><tr>';
|
|
167
|
+
for (const h of headers) html += `<th>${parseInline(h, outgoingLinks)}</th>`;
|
|
168
|
+
html += '</tr></thead><tbody>';
|
|
169
|
+
for (const row of rows) {
|
|
170
|
+
html += '<tr>';
|
|
171
|
+
for (const cell of row) html += `<td>${parseInline(cell, outgoingLinks)}</td>`;
|
|
172
|
+
html += '</tr>';
|
|
173
|
+
}
|
|
174
|
+
html += '</tbody></table></div>';
|
|
175
|
+
return html;
|
|
176
|
+
}
|
|
177
|
+
|
|
176
178
|
function escapeHtml(str) {
|
|
177
|
-
return str
|
|
178
|
-
.replace(/&/g, '&')
|
|
179
|
-
.replace(/</g, '<')
|
|
180
|
-
.replace(/>/g, '>')
|
|
181
|
-
.replace(/"/g, '"');
|
|
179
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
182
180
|
}
|
|
183
181
|
|
|
184
182
|
function escapeAttr(str) {
|