+282
-98
public/converter.html
+282
-98
public/converter.html
···
1
1
<!-- This file is written by ChatGPT. I am lazy. -->
2
-
3
2
<!DOCTYPE html>
4
3
<html lang="en">
5
4
<head>
6
-
<meta charset="UTF-8">
7
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8
-
<title>Markdown to Post Format</title>
9
-
<script src="https://cdn.jsdelivr.net/npm/showdown@1.9.1/dist/showdown.min.js"></script>
5
+
<meta charset="UTF-8">
6
+
<title>Markdown Editor to ATProto JSON</title>
7
+
<!-- Toast UI Editor CSS -->
8
+
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
9
+
<style>
10
+
body {
11
+
margin: 0;
12
+
font-family: Arial, sans-serif;
13
+
display: flex;
14
+
height: 100vh;
15
+
}
16
+
.pane {
17
+
flex: 1;
18
+
padding: 1em;
19
+
overflow-y: auto;
20
+
border-right: 1px solid #ccc;
21
+
}
22
+
.json-pane {
23
+
background: #f6f6f6;
24
+
font-family: monospace;
25
+
white-space: pre-wrap;
26
+
padding: 1em;
27
+
position: relative;
28
+
}
29
+
.title-input {
30
+
width: calc(100% - 16px);
31
+
margin: 0.5em;
32
+
font-size: 1.2em;
33
+
padding: 8px;
34
+
}
35
+
#editorContainer {
36
+
height: calc(100% - 60px);
37
+
}
38
+
/* Copy button styling */
39
+
#copyButton {
40
+
position: absolute;
41
+
top: 10px;
42
+
right: 10px;
43
+
padding: 6px 12px;
44
+
font-size: 0.9em;
45
+
background-color: #007bff;
46
+
color: white;
47
+
border: none;
48
+
border-radius: 4px;
49
+
cursor: pointer;
50
+
}
51
+
#copyButton:hover {
52
+
background-color: #0056b3;
53
+
}
54
+
</style>
10
55
</head>
11
56
<body>
12
-
<h1>Markdown to Post Format Converter</h1>
57
+
<!-- Left Pane: Markdown Editor -->
58
+
<div class="pane">
59
+
<input type="text" id="title" class="title-input" placeholder="Enter Title Here" value="Nostr and ATProto - Part 1">
60
+
<div id="editorContainer"></div>
61
+
</div>
13
62
14
-
<textarea id="markdownInput" rows="10" cols="50" placeholder="Enter your Markdown here..."></textarea><br>
15
-
<button id="convertButton">Convert</button>
63
+
<!-- Right Pane: JSON Output with Copy Button -->
64
+
<div class="pane json-pane" id="jsonOutput">
65
+
<button id="copyButton">Copy JSON</button>
66
+
<!-- Live JSON output appears here -->
67
+
</div>
16
68
17
-
<h2>Converted Post Format:</h2>
18
-
<pre id="output"></pre>
69
+
<!-- Toast UI Editor JS -->
70
+
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
71
+
<!-- markdown-it for parsing markdown -->
72
+
<script src="https://cdn.jsdelivr.net/npm/markdown-it@12.2.0/dist/markdown-it.min.js"></script>
73
+
<script>
74
+
// Initialize Toast UI Editor in Markdown mode.
75
+
const editor = new toastui.Editor({
76
+
el: document.querySelector('#editorContainer'),
77
+
initialEditType: 'markdown',
78
+
previewStyle: 'vertical',
79
+
height: '100%',
80
+
});
19
81
20
-
<script>
21
-
// Initialize Showdown (Markdown to HTML)
22
-
const converter = new showdown.Converter();
82
+
// Create a markdown-it instance.
83
+
const md = window.markdownit({
84
+
html: false,
85
+
linkify: true,
86
+
typographer: true,
87
+
});
23
88
24
-
// Mapping Markdown to your custom format
25
-
function convertMarkdownToCustomFormat(markdown) {
26
-
const lines = markdown.split("\n");
27
-
const result = [];
89
+
// Update the JSON output on editor changes or title changes.
90
+
document.getElementById('title').addEventListener('input', updateJson);
91
+
editor.on('change', updateJson);
28
92
29
-
let currentParagraph = [];
30
-
let currentFormatting = null;
93
+
// Copy button event listener.
94
+
document.getElementById('copyButton').addEventListener('click', () => {
95
+
const jsonText = document.getElementById('jsonOutput').textContent;
96
+
navigator.clipboard.writeText(jsonText)
97
+
.then(() => {
98
+
alert('JSON copied to clipboard!');
99
+
})
100
+
.catch(err => {
101
+
alert('Failed to copy JSON: ' + err);
102
+
});
103
+
});
31
104
32
-
lines.forEach(line => {
33
-
if (line.startsWith('# ')) {
34
-
// Heading
35
-
result.push({
36
-
text: line.substring(2),
37
-
$type: "net.shreyanjain.blog.richtext#heading"
38
-
});
39
-
} else if (line.startsWith('## ')) {
40
-
// Subheading
41
-
result.push({
42
-
text: line.substring(3),
43
-
$type: "net.shreyanjain.blog.richtext#subheading"
44
-
});
45
-
} else if (line.startsWith(')
47
-
const altText = line.match(/\[([^\]]+)\]/)[1];
48
-
const url = line.match(/\(([^)]+)\)/)[1];
49
-
result.push({
50
-
text: {
51
-
alt: altText,
52
-
img: url,
53
-
source: "url",
54
-
dimensions: { width: 200, height: 150 }
55
-
},
56
-
$type: "net.shreyanjain.blog.richtext#image"
57
-
});
58
-
} else {
59
-
// Paragraph or normal text
60
-
const parts = line.split(/(\*\*.*?\*\*|\*.*?\*|_[^_]+_|~~.*?~~|\[.*?\]\(.*?\))/g); // Splits on Markdown formatting
105
+
// The main conversion function: parses markdown to tokens and converts to our custom JSON.
106
+
function updateJson() {
107
+
const markdown = editor.getMarkdown();
108
+
const title = document.getElementById('title').value;
109
+
// Parse the markdown into tokens using markdown-it.
110
+
const tokens = md.parse(markdown, {});
111
+
const content = convertTokensToJson(tokens);
61
112
62
-
parts.forEach(part => {
63
-
if (part.startsWith("**")) {
64
-
// Bold text
65
-
currentParagraph.push({
66
-
content: part.slice(2, -2),
67
-
formatting: [{ "$type": "net.shreyanjain.blog.richtext#bold" }]
68
-
});
69
-
} else if (part.startsWith("*") || part.startsWith("_")) {
70
-
// Italic text
71
-
currentParagraph.push({
72
-
content: part.slice(1, -1),
73
-
formatting: [{ "$type": "net.shreyanjain.blog.richtext#italic" }]
74
-
});
75
-
} else if (part.startsWith("~~")) {
76
-
// Strikethrough text (if desired)
77
-
currentParagraph.push({
78
-
content: part.slice(2, -2),
79
-
formatting: [{ "$type": "net.shreyanjain.blog.richtext#strikethrough" }]
80
-
});
81
-
} else if (part.startsWith("[") && part.includes("](")) {
82
-
// Link (Markdown: [text](url))
83
-
const linkText = part.match(/\[([^\]]+)\]/)[1];
84
-
const linkUrl = part.match(/\(([^)]+)\)/)[1];
85
-
currentParagraph.push({
86
-
content: linkText,
87
-
formatting: [{
88
-
"$type": "net.shreyanjain.blog.richtext#link",
89
-
"href": linkUrl
90
-
}]
91
-
});
92
-
} else if (part.trim()) {
93
-
// Normal text
94
-
currentParagraph.push({ content: part });
95
-
}
96
-
});
113
+
const jsonOutput = {
114
+
"$type": "net.shreyanjain.blog.post",
115
+
"title": title,
116
+
"content": content,
117
+
"createdAt": new Date().toISOString()
118
+
};
97
119
98
-
result.push({
99
-
text: currentParagraph,
100
-
$type: "net.shreyanjain.blog.richtext#paragraph"
101
-
});
102
-
currentParagraph = [];
103
-
}
104
-
});
120
+
document.getElementById('jsonOutput').childNodes.forEach(node => {
121
+
if(node.nodeType === Node.TEXT_NODE) node.remove();
122
+
});
123
+
124
+
// Update the jsonOutput's text content (the copy button remains unaffected).
125
+
document.getElementById('jsonOutput').lastChild.textContent = JSON.stringify(jsonOutput, null, 2);
126
+
// Instead, update the JSON text by replacing all text except the copy button.
127
+
const copyButton = document.getElementById('copyButton');
128
+
document.getElementById('jsonOutput').innerHTML = "";
129
+
document.getElementById('jsonOutput').appendChild(copyButton);
130
+
const pre = document.createElement('pre');
131
+
pre.textContent = JSON.stringify(jsonOutput, null, 2);
132
+
document.getElementById('jsonOutput').appendChild(pre);
133
+
}
134
+
135
+
// Converts markdown-it tokens into your custom JSON blocks.
136
+
function convertTokensToJson(tokens) {
137
+
const blocks = [];
138
+
let i = 0;
139
+
140
+
while(i < tokens.length) {
141
+
const token = tokens[i];
142
+
143
+
// Process headings with raw text output.
144
+
if(token.type === 'heading_open') {
145
+
const level = parseInt(token.tag.slice(1));
146
+
const inlineToken = tokens[i+1]; // inline token holds the heading text
147
+
// Process inline tokens and join them to a string.
148
+
const textContent = processInline(inlineToken.children).map(seg => seg.content).join('');
149
+
const block = {
150
+
"text": textContent,
151
+
"$type": level === 1
152
+
? "net.shreyanjain.blog.richtext#heading"
153
+
: "net.shreyanjain.blog.richtext#subheading"
154
+
};
155
+
blocks.push(block);
156
+
i += 3; // skip heading_open, inline, heading_close
157
+
continue;
158
+
}
159
+
160
+
// Process paragraphs (still using content arrays).
161
+
if(token.type === 'paragraph_open') {
162
+
const inlineToken = tokens[i+1]; // inline token for paragraph
163
+
const textBlocks = processInline(inlineToken.children);
164
+
const block = {
165
+
"text": textBlocks,
166
+
"$type": "net.shreyanjain.blog.richtext#paragraph"
167
+
};
168
+
blocks.push(block);
169
+
i += 3; // skip paragraph_open, inline, paragraph_close
170
+
continue;
171
+
}
105
172
106
-
return result;
173
+
// Process code blocks (fence)
174
+
if(token.type === 'fence') {
175
+
const block = {
176
+
"text": token.content,
177
+
"$type": "net.shreyanjain.blog.richtext#code"
178
+
};
179
+
blocks.push(block);
180
+
i++;
181
+
continue;
107
182
}
108
183
109
-
// Handle the button click event
110
-
document.getElementById("convertButton").addEventListener("click", () => {
111
-
const markdownInput = document.getElementById("markdownInput").value;
112
-
const customFormat = convertMarkdownToCustomFormat(markdownInput);
113
-
document.getElementById("output").textContent = JSON.stringify(customFormat, null, 4);
114
-
});
115
-
</script>
184
+
// Process lists (both bullet and ordered)
185
+
if(token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
186
+
const listItems = [];
187
+
i++; // advance into list items
188
+
while(i < tokens.length && tokens[i].type !== 'bullet_list_close' && tokens[i].type !== 'ordered_list_close') {
189
+
if(tokens[i].type === 'list_item_open') {
190
+
// Each list item is expected to have a paragraph inside.
191
+
i++; // move into list item content
192
+
let itemContent = [];
193
+
if(tokens[i].type === 'paragraph_open') {
194
+
const inlineToken = tokens[i+1];
195
+
itemContent = processInline(inlineToken.children);
196
+
i += 3; // skip paragraph tokens
197
+
}
198
+
listItems.push(itemContent);
199
+
while(i < tokens.length && tokens[i].type !== 'list_item_close') { i++; }
200
+
i++; // skip list_item_close
201
+
} else {
202
+
i++;
203
+
}
204
+
}
205
+
// Wrap list items in a list block.
206
+
const block = {
207
+
"text": listItems,
208
+
"$type": "net.shreyanjain.blog.richtext#list"
209
+
};
210
+
blocks.push(block);
211
+
i++; // skip list_close
212
+
continue;
213
+
}
214
+
215
+
// If token not matched, advance.
216
+
i++;
217
+
}
218
+
219
+
return blocks;
220
+
}
221
+
222
+
// Process inline tokens to extract text segments and formatting.
223
+
function processInline(tokens) {
224
+
const segments = [];
225
+
let i = 0;
226
+
227
+
while (i < tokens.length) {
228
+
const token = tokens[i];
229
+
230
+
if (token.type === 'text') {
231
+
segments.push({ "content": token.content });
232
+
i++;
233
+
} else if (token.type === 'code_inline') {
234
+
segments.push({
235
+
"content": token.content,
236
+
"formatting": [
237
+
{ "$type": "net.shreyanjain.blog.richtext#code-inline" }
238
+
]
239
+
});
240
+
i++;
241
+
} else if (token.type === 'em_open') {
242
+
let content = "";
243
+
i++;
244
+
while(i < tokens.length && tokens[i].type !== 'em_close') {
245
+
if(tokens[i].content) content += tokens[i].content;
246
+
i++;
247
+
}
248
+
i++; // Skip 'em_close'
249
+
segments.push({
250
+
"content": content,
251
+
"formatting": [
252
+
{ "$type": "net.shreyanjain.blog.richtext#italic" }
253
+
]
254
+
});
255
+
} else if (token.type === 'strong_open') {
256
+
let content = "";
257
+
i++;
258
+
while(i < tokens.length && tokens[i].type !== 'strong_close') {
259
+
if(tokens[i].content) content += tokens[i].content;
260
+
i++;
261
+
}
262
+
i++; // Skip 'strong_close'
263
+
segments.push({
264
+
"content": content,
265
+
"formatting": [
266
+
{ "$type": "net.shreyanjain.blog.richtext#bold" }
267
+
]
268
+
});
269
+
} else if (token.type === 'link_open') {
270
+
let href = "";
271
+
if (token.attrs) {
272
+
const hrefAttr = token.attrs.find(attr => attr[0] === 'href');
273
+
if (hrefAttr) href = hrefAttr[1];
274
+
}
275
+
let content = "";
276
+
i++;
277
+
while(i < tokens.length && tokens[i].type !== 'link_close') {
278
+
if(tokens[i].content) content += tokens[i].content;
279
+
i++;
280
+
}
281
+
i++; // Skip 'link_close'
282
+
segments.push({
283
+
"content": content,
284
+
"formatting": [
285
+
{ "$type": "net.shreyanjain.blog.richtext#link", "href": href }
286
+
]
287
+
});
288
+
} else {
289
+
// Skip any unhandled tokens.
290
+
i++;
291
+
}
292
+
}
293
+
294
+
return segments;
295
+
}
296
+
297
+
// Initial call
298
+
updateJson();
299
+
</script>
116
300
</body>
117
301
</html>