Просмотр исходного кода

Sorta kinda working JS parser before I rework it completely

main
Rocketsoup 1 год назад
Сommit
1409a97c7e
4 измененных файлов: 478 добавлений и 0 удалений
  1. 1
    0
      .gitignore
  2. 400
    0
      js/markdown.js
  3. 77
    0
      markdownjs.html
  4. 0
    0
      markdownphp.php

+ 1
- 0
.gitignore Просмотреть файл

@@ -0,0 +1 @@
1
+.DS_Store

+ 400
- 0
js/markdown.js Просмотреть файл

@@ -0,0 +1,400 @@
1
+class MDState {
2
+	/** @var {object} */
3
+	abbreviations = {};
4
+
5
+	/** @var {object} */
6
+	footnotes = {};
7
+}
8
+
9
+class MDConfig {
10
+
11
+}
12
+
13
+class MDBlock {
14
+	/**
15
+	 * @returns {String} HTML
16
+	 */
17
+	toHTML(config, state) {
18
+		throw new Error("toHTML not implemented");
19
+	}
20
+}
21
+
22
+class MDHTMLBlock extends MDBlock {
23
+	/** @var {String} html */
24
+	html;
25
+
26
+	/**
27
+	 * @param {String} html
28
+	 */
29
+	constructor(html) {
30
+		super();
31
+		this.html = html;
32
+	}
33
+
34
+	toHTML = (config, state) => this.html;
35
+}
36
+
37
+class MDHTMLWrappedBlock extends MDBlock {
38
+	/** @var {String} */
39
+	#beforeHTML;
40
+	/** @var {MDBlock} */
41
+	#content;
42
+	/** @var {String} */
43
+	#afterHTML;
44
+
45
+	/**
46
+	 * @param {String} beforeHTML
47
+	 * @param {MDBlock} content
48
+	 * @param {String} afterHTML
49
+	 */
50
+	constructor(beforeHTML, content, afterHTML) {
51
+		super();
52
+		if (!(content instanceof MDBlock)) {
53
+			throw new Error("content is of type " + typeof(content) + " instead of MDBlock");
54
+		}
55
+		this.#beforeHTML = beforeHTML;
56
+		this.#content = content;
57
+		this.#afterHTML = afterHTML;
58
+	}
59
+
60
+	toHTML(config, state) {
61
+		return this.#beforeHTML + this.#content.toHTML() + this.#afterHTML;
62
+	}
63
+}
64
+
65
+class MDInlineBlock extends MDBlock {
66
+	/** @var {String} */
67
+	#html;
68
+
69
+	/**
70
+	 * @param {String} line
71
+	 */
72
+	constructor(line) {
73
+		super();
74
+		this.#html = line;
75
+	}
76
+
77
+	toHTML = (config, state) => "inline:" + this.#html;
78
+}
79
+
80
+class MDUnprocessedLinesBlock extends MDBlock {
81
+	/** @var {MDBlock[]} */
82
+	#blocks = [];
83
+
84
+	/**
85
+	 * @param {String[]} lines
86
+	 */
87
+	constructor(lines) {
88
+		super();
89
+		// Find markers that always start a new block
90
+		let blockQuoteBlocks = MDUnprocessedLinesBlock.findBlockQuote(lines);
91
+		if (blockQuoteBlocks) {
92
+			this.#blocks = blockQuoteBlocks;
93
+			return;
94
+		}
95
+		let headerBlocks = MDUnprocessedLinesBlock.findHeader(lines);
96
+		if (headerBlocks) {
97
+			this.#blocks = headerBlocks;
98
+			return;
99
+		}
100
+		let codeLines = MDUnprocessedLinesBlock.findTickCodeBlock(lines);
101
+		if (codeLines) {
102
+			this.#blocks = codeLines;
103
+			return;
104
+		}
105
+		let codeLines0 = MDUnprocessedLinesBlock.findIndentCodeBlock(lines);
106
+		if (codeLines0) {
107
+			this.#blocks = codeLines0;
108
+			return;
109
+		}
110
+		// Find runs of contiguous non-blank lines
111
+		var contiguousLines = [];
112
+		let blankRegex = /^\s*$/;
113
+		for (const line of lines) {
114
+			if (blankRegex.exec(line)) {
115
+				if (contiguousLines.length > 0) {
116
+					this.#blocks.push(new MDContiguousUnprocessedLinesBlock(contiguousLines));
117
+				}
118
+				contiguousLines = [];
119
+			} else {
120
+				contiguousLines.push(line);
121
+			}
122
+		}
123
+		if (contiguousLines.length > 0) {
124
+			this.#blocks.push(new MDContiguousUnprocessedLinesBlock(contiguousLines));
125
+		}
126
+	}
127
+
128
+	/**
129
+	 * @param {String[]} lines
130
+	 * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the blockquoted content, or null if not found
131
+	 */
132
+	static findBlockQuote(lines) {
133
+		var portion = 0;
134
+		var beforeLines = [];
135
+		var blockQuoteLines = [];
136
+		var afterLines = [];
137
+		for (const line of lines) {
138
+			switch (portion) {
139
+				case 0:
140
+					if (line.startsWith(">")) {
141
+						blockQuoteLines.push(line.substring(1));
142
+						portion = 1;
143
+					} else {
144
+						beforeLines.push(line);
145
+					}
146
+					break;
147
+				case 1:
148
+					if (line.startsWith(">")) {
149
+						blockQuoteLines.push(line.substring(1));
150
+					} else {
151
+						afterLines.push(line);
152
+						portion = 2;
153
+					}
154
+					break;
155
+				case 2:
156
+					afterLines.push(line);
157
+					break;
158
+			}
159
+		}
160
+		if (blockQuoteLines.length == 0) {
161
+			return null;
162
+		}
163
+		var blocks = [];
164
+		if (beforeLines.length > 0) {
165
+			blocks.push(new MDUnprocessedLinesBlock(beforeLines));
166
+		}
167
+		blocks.push(new MDHTMLWrappedBlock(
168
+			"<blockquote>\n",
169
+			new MDUnprocessedLinesBlock(Markdown.trimEvenly(blockQuoteLines)),
170
+			"</blockquote>\n"));
171
+		if (afterLines.length > 0) {
172
+			blocks.push(new MDUnprocessedLinesBlock(afterLines));
173
+		}
174
+		return blocks;
175
+	}
176
+
177
+	/**
178
+	 * @param {String[]} lines
179
+	 * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the header content, or null if not found
180
+	 */
181
+	static findHeader(lines) {
182
+		var portion = 0;
183
+		var beforeLines = [];
184
+		var headerBlock = null;
185
+		var afterLines = [];
186
+		for (const line of lines) {
187
+			let hashMatch = /^\s*(#{1,6})\s*(.*)$/.exec(line);
188
+			let dashMatch = /^(-+|=+)$/.exec(line);
189
+			switch (portion) {
190
+				case 0:
191
+					if (hashMatch) {
192
+						let headerLevel = hashMatch[1].length;
193
+						let contentMarkdown = hashMatch[2];
194
+						headerBlock = new MDHTMLWrappedBlock('<h' + headerLevel + '>', new MDInlineBlock(contentMarkdown), '</h' + headerLevel + ">\n");
195
+						portion = 1;
196
+					} else if (dashMatch && beforeLines.length > 1) {
197
+						let contentMarkdown = beforeLines.pop();
198
+						let headerLevel = dashMatch[1].startsWith("=") ? 1 : 2;
199
+						headerBlock = new MDHTMLWrappedBlock('<h' + headerLevel + '>', new MDInlineBlock(contentMarkdown), '</h' + headerLevel + ">\n");
200
+						portion = 1;
201
+					} else {
202
+						beforeLines.push(line);
203
+					}
204
+					break;
205
+				case 1:
206
+					afterLines.push(line);
207
+					break;
208
+			}
209
+		}
210
+		if (headerBlock == null) {
211
+			return null;
212
+		}
213
+		var blocks = [];
214
+		if (beforeLines.length > 0) {
215
+			blocks.push(new MDUnprocessedLinesBlock(beforeLines));
216
+		}
217
+		blocks.push(headerBlock);
218
+		if (afterLines.length > 0) {
219
+			blocks.push(new MDUnprocessedLinesBlock(afterLines));
220
+		}
221
+		return blocks;
222
+	}
223
+
224
+	/**
225
+	 * @param {String[]} lines
226
+	 * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the code content, or null if not found
227
+	 */
228
+	static findTickCodeBlock(lines) {
229
+		var portion = 0;
230
+		var beforeLines = [];
231
+		var codeLines = [];
232
+		var afterLines = [];
233
+		for (const line of lines) {
234
+			switch (portion) {
235
+				case 0:
236
+					if (/^\s*```\s*$/.exec(line)) {
237
+						portion = 1;
238
+					} else {
239
+						beforeLines.push(line);
240
+					}
241
+					break;
242
+				case 1:
243
+					if (/^\s*```\s*$/.exec(line)) {
244
+						portion = 2;
245
+					} else {
246
+						codeLines.push(line);
247
+					}
248
+					break;
249
+				case 2:
250
+					afterLines.push(line);
251
+					break;
252
+			}
253
+		}
254
+		if (codeLines.length == 0) return null;
255
+		var blocks = [];
256
+		if (beforeLines.length > 0) {
257
+			blocks.push(new MDUnprocessedLinesBlock(beforeLines));
258
+		}
259
+		blocks.push(new MDHTMLWrappedBlock("<pre>", MDHTMLBlock(codeLines.join("\n")), "</pre>\n"));
260
+		if (afterLines.length > 0) {
261
+			blocks.push(new MDUnprocessedLinesBlock(afterLines));
262
+		}
263
+		return blocks;
264
+	}
265
+
266
+	/**
267
+	 * @param {String[]} lines
268
+	 * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the code content, or null if not found
269
+	 */
270
+	static findIndentCodeBlock(lines) {
271
+		var portion = 0;
272
+		var beforeLines = [];
273
+		var codeLines = [];
274
+		var afterLines = [];
275
+		let regex = /^(\s{4,})(.*)$/;
276
+		var minIndent = 999999;
277
+		for (const line of lines) {
278
+			let indentMatch = regex.exec(line);
279
+			switch (portion) {
280
+				case 0:
281
+					if (indentMatch) {
282
+						minIndent = Math.min(minIndent, indentMatch[1].length);
283
+						codeLines.push(line);
284
+						portion = 1;
285
+					} else {
286
+						beforeLines.push(line);
287
+					}
288
+					break;
289
+				case 1:
290
+					if (indentMatch) {
291
+						minIndent = Math.min(minIndent, indentMatch[1].length);
292
+						codeLines.push(line);
293
+					} else {
294
+						afterLines.push(line);
295
+						portion = 2;
296
+					}
297
+					break;
298
+				case 2:
299
+					afterLines.push(line);
300
+					break;
301
+			}
302
+		}
303
+		if (codeLines.length == 0) return null;
304
+		var blocks = [];
305
+		if (beforeLines.length > 0) {
306
+			blocks.push(new MDUnprocessedLinesBlock(beforeLines));
307
+		}
308
+		blocks.push(new MDHTMLWrappedBlock("<pre>", new MDHTMLBlock(codeLines.map((l) => l.substring(minIndent)).join("\n")), "</pre>\n"));
309
+		if (afterLines.length > 0) {
310
+			blocks.push(new MDUnprocessedLinesBlock(afterLines));
311
+		}
312
+		return blocks;
313
+	}
314
+
315
+	toHTML(config, state) {
316
+		var html = "";
317
+		for (const block of this.#blocks) {
318
+			html += block.toHTML() + "\n";
319
+		}
320
+		return html;
321
+	}
322
+}
323
+
324
+class MDContiguousUnprocessedLinesBlock extends MDBlock {
325
+	/** @var {MDBlock[]} */
326
+	#blocks;
327
+
328
+	/**
329
+	 * @param {String[]} lines
330
+	 */
331
+	constructor(lines) {
332
+		super();
333
+		this.#blocks = [ new MDHTMLWrappedBlock('<p>contiguous: ', new MDInlineBlock(lines.join(' ')), "</p>\n\n") ];
334
+	}
335
+
336
+	toHTML(config, state) {
337
+		var html = "";
338
+		for (const block of this.#blocks) {
339
+			html += block.toHTML() + "\n";
340
+		}
341
+		return html;
342
+	}
343
+}
344
+
345
+class Markdown {
346
+	/**
347
+	 * @param  {String} markdown
348
+	 * @returns {String} HTML
349
+	 */
350
+	static toHTML(markdown, config=new MDConfig()) {
351
+		// Blocks that immediately start a new block
352
+		// - Headers
353
+		// - Blockquote
354
+		// - Code block   ```\ncode\n```
355
+		// Blocks that need blank line first
356
+		// - HR   ---   - - -   ***   * * * * * *
357
+		// - Lists
358
+		// - Table
359
+		// - Code block   [4+spaces]code
360
+		// - Definition list   term\n: definition\n: alternate def
361
+		// Unknown blocks
362
+		// - Footnotes   some text[^1]   [^1]: first footnote content
363
+		// - Abbreviations   *[HTML]: Hyper Text
364
+		// Inline styles
365
+		// - Links
366
+		// - Italic
367
+		// - Bold
368
+		// - `code`
369
+		// - Strikethrough
370
+		// - Images   ![alt text](url){.cssclass}
371
+		// - Literals   \*
372
+
373
+		let state = new MDState();
374
+		let lines = markdown.trim().replace("\r", "").split("\n");
375
+		return new MDUnprocessedLinesBlock(lines).toHTML(config, state);
376
+	}
377
+
378
+	/**
379
+	 * @param {String[]} lines
380
+	 * @returns {String[]}
381
+	 */
382
+	static trimEvenly(lines) {
383
+		var minIndent = 999999;
384
+		let regex = /^(\s*)($|\S.*$)/;
385
+		for (const line of lines) {
386
+			let groups = regex.exec(line);
387
+			let indent = groups[1].length;
388
+			if (groups[2].trim().length > 0 && indent < 4) {
389
+				minIndent = Math.min(minIndent, indent);
390
+			}
391
+		}
392
+		if (minIndent == 0) return lines;
393
+		var trimmed = [];
394
+		let trimRegex = new RegExp(`^\s{${minIndent}}`);
395
+		for (const line of lines) {
396
+			trimmed.push(line.replace(trimRegex, ''));
397
+		}
398
+		return trimmed;
399
+	}
400
+}

+ 77
- 0
markdownjs.html Просмотреть файл

@@ -0,0 +1,77 @@
1
+<!DOCTYPE html>
2
+<html>
3
+	<head>
4
+		<meta charset="utf-8">
5
+		<title>Markdown Test</title>
6
+		<script src="js/markdown.js"></script>
7
+		<script>
8
+			function onDocumentLoad() {
9
+				document.getElementById('markdowninput').addEventListener('input', onMarkdownChange);
10
+			}
11
+			function onMarkdownChange() {
12
+				let markdown = document.getElementById('markdowninput').value;
13
+				let html = Markdown.toHTML(markdown);
14
+				document.getElementById('preview').innerHTML = html;
15
+			}
16
+
17
+			document.addEventListener('DOMContentLoaded', onDocumentLoad);
18
+		</script>
19
+		<style>
20
+			:root, body {
21
+				width: 100%;
22
+				height: 100%;
23
+				padding: 0;
24
+				margin: 0;
25
+			}
26
+			.previewhalf {
27
+				position: relative;
28
+				width: 100%;
29
+				height: 50%;
30
+				background-color: white;
31
+				overflow-y: auto;
32
+			}
33
+			.inputhalf {
34
+				position: relative;
35
+				width: 100%;
36
+				height: 50%;
37
+				background-color: #0000aa;
38
+				color: white;
39
+			}
40
+			#preview {
41
+				padding: 1em;
42
+				margin: 0 auto;
43
+				max-width: 600px;
44
+			}
45
+			#markdowninput {
46
+				width: 100%;
47
+				height: 100%;
48
+				box-sizing: border-box;
49
+				margin: 0;
50
+				padding: 0.5em;
51
+				border: 0;
52
+				background-color: #0000aa;
53
+				color: white;
54
+			}
55
+		</style>
56
+	</head>
57
+
58
+	<body>
59
+		<div class="previewhalf half">
60
+			<div id="preview">
61
+				<p>Preview here</p>
62
+			</div>
63
+		</div>
64
+		<div class="inputhalf half">
65
+			<textarea id="markdowninput">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mollis vel metus ut tempor. Morbi dictum, velit quis venenatis consequat, ante leo dignissim magna, quis efficitur ex magna nec ex. In egestas aliquet tortor, vitae posuere dui lobortis ac. Pellentesque quis hendrerit lorem. In maximus ut sapien vel tristique. Pellentesque ut tincidunt orci. Phasellus dignissim massa sit amet tellus placerat finibus. Ut elementum tortor ex, non aliquam libero semper eu.
66
+
67
+Duis gravida convallis nisi ullamcorper tempor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc bibendum varius arcu non molestie. Ut cursus, lorem nec pellentesque interdum, augue risus pellentesque ligula, at vehicula turpis mi vitae nisi. Vivamus pulvinar eget lorem non lacinia. Pellentesque pretium ex at metus elementum semper. Donec tincidunt metus ac ex aliquam, ac iaculis purus facilisis. Phasellus iaculis scelerisque nisl, eu molestie dui sollicitudin sed. Suspendisse ut felis porttitor, sollicitudin urna in, venenatis neque. Donec nec mi ultrices, molestie quam quis, iaculis libero.
68
+
69
+Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut congue consequat vestibulum. Etiam luctus, leo placerat ullamcorper venenatis, nunc metus fringilla sapien, id elementum tellus nulla eu nunc. Integer pulvinar egestas scelerisque. Sed accumsan libero eget pretium sodales. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla pretium egestas elit id cursus. Morbi euismod elementum eros ut eleifend. Praesent velit sem, faucibus sed sapien sit amet, pellentesque fermentum nibh. Nulla consequat turpis nec lacus consequat, sed congue magna consectetur. Nam id urna rhoncus, consectetur eros ac, tempor nisl.
70
+
71
+Curabitur fermentum nunc sed dapibus feugiat. Etiam volutpat viverra eros sit amet tristique. Suspendisse cursus venenatis accumsan. Suspendisse venenatis vehicula lectus, sed venenatis augue pretium eu. Cras dui eros, iaculis in dictum hendrerit, tincidunt ut sapien. Ut nec nulla ut mauris cursus tincidunt. Suspendisse interdum pulvinar elit sit amet auctor. Donec id lacus tincidunt, varius elit non, tempor felis.
72
+
73
+Quisque posuere, libero ac elementum maximus, lacus nulla lobortis nibh, id aliquam tortor lacus vel quam. Nunc eget dolor non nunc sagittis varius. Proin viverra vestibulum placerat. Fusce placerat interdum diam quis sollicitudin. Praesent sed tincidunt orci. Suspendisse lectus lorem, porta vel quam sit amet, pharetra interdum purus. Suspendisse ac lacus consequat, dapibus nunc in, porttitor purus. Nam dictum elit eget massa tincidunt tempus. Vestibulum blandit, lorem et accumsan tristique, orci dolor vulputate magna, et posuere lorem sapien non sapien. Vivamus nulla justo, eleifend et metus sit amet, gravida iaculis erat. Fusce lacinia cursus accumsan. 
74
+			</textarea>
75
+		</div>
76
+	</body>
77
+</html>

+ 0
- 0
markdownphp.php Просмотреть файл


Загрузка…
Отмена
Сохранить