markdown-maker 1.10.2 → 1.10.3

Sign up to get free protection for your applications and to get access to all the features.
package/src/parser.ts CHANGED
@@ -1,370 +1,398 @@
1
1
  import fs from "fs"; /* for handling reading of files */
2
2
  import path from "path"; /* for handling file paths */
3
3
 
4
- import Colors = require("colors.ts"); /* for adding colours to strings */
5
- Colors.enable();
6
4
  import marked from "marked";
7
5
 
8
- import { Command, commands, load_extensions, MDMError } from "./commands";
9
- import { argParser, CLArgs as CommandLineArgs, ParserOptions } from "./cltool";
10
-
11
- enum TargetType {
12
- HTML,
13
- MARKDOWN,
14
- }
6
+ import { Command, commands, load_extensions } from "./commands";
7
+ import {
8
+ argParser,
9
+ IncompleteCommandLineArgs,
10
+ IncompleteParserOptions,
11
+ ParserOptions,
12
+ } from "./cltool";
13
+ import { MDMError, MDMNonParserError, MDMWarning } from "./errors";
14
+ import { CommandGroupType, TaggedElement, TargetType } from "./types";
15
15
 
16
16
  /* parse some md
17
17
  * recursively with extra options */
18
18
  class Parser {
19
- file: string;
20
- parent?: Parser;
21
- line_num: number;
22
- wd: string;
23
- wd_full: string;
24
- blobs: {
25
- [key: number]: string | undefined;
26
- };
27
- opts: ParserOptions;
28
- clargs: CommandLineArgs;
29
- raw: string;
30
-
31
- static TOKEN = "#md";
32
-
33
- constructor(
34
- filename: string,
35
- clargs?: CommandLineArgs,
36
- opts?: {
37
- parent?: Parser;
38
- isFileCallback?: (s: string) => false | string;
39
- }
40
- ) {
41
- /* this.working_directory */
42
- this.file = filename;
43
-
44
- if (!opts) opts = {};
45
-
46
- /* the parent parser */
47
- this.parent = opts.parent;
48
-
49
- this.line_num = 0;
50
- this.wd = path.dirname(filename);
51
- this.wd_full = path.resolve(this.wd);
52
-
53
- /* finished blob */
54
- this.blobs = {};
55
-
56
- /* all options */
57
- this.clargs = clargs;
58
- this.opts = {
59
- defs: {},
60
- secs: [],
61
- args: [],
62
- depth: 0,
63
- verbose: false,
64
- debug: false,
65
- max_depth: 5,
66
- use_underscore: false,
67
- toc_level: 3,
68
- allow_undefined: false,
69
- html: false,
70
- watch: false,
71
- targetType: undefined,
72
- only_warn: false,
73
- parent: undefined,
74
- hooks: {},
75
- adv_hooks: {},
76
- isFileCallback: (f) => {
77
- if (!fs.existsSync(f)) return false;
78
- return fs.readFileSync(f, "utf-8") + "\n";
79
- },
80
- };
81
-
82
- if (!clargs) {
83
- clargs = argParser.parse_args([filename]);
84
- }
85
-
86
- /* append all commandline arguments to this */
87
- Object.assign(this.opts, clargs);
88
- Object.assign(this.opts, opts);
89
-
90
- this.raw = this.opts.isFileCallback(filename) || filename;
91
- }
92
-
93
- /**
94
- * parse wrapper for handling
95
- * preprocessing, parsing and postprocess
96
- **/
97
- parse() {
98
- load_extensions(this);
99
- if (this.opts.verbose || this.opts.debug) {
100
- console.log(
101
- Colors.colors(
102
- "magenta",
103
- "parsing " + this.file + ": depth=" + this.opts.depth
104
- )
105
- );
106
- }
107
-
108
- if (this.opts.debug) {
109
- console.log("Parsing options:");
110
- console.log(this.opts);
111
- }
112
-
113
- /* reset sections for beginning parse */
114
- if (this.opts.depth === 0) this.opts.secs = [];
115
- let __blob;
116
-
117
- /* apply preproccessing to raw file */
118
- __blob = this.preprocess(this.raw);
119
-
120
- /* main parser instance call */
121
- __blob = this.mainparse(__blob);
122
-
123
- /**
124
- * apply postprocessing after */
125
- __blob = this.postprocess(__blob);
126
-
127
- return __blob;
128
- }
129
-
130
- mainparse(blob: string) {
131
- if (this.opts.verbose || this.opts.debug) {
132
- console.debug(`beginning mainparse of '${this.file}'`.blue);
133
- }
134
-
135
- /* main parser instance loop */
136
- blob.split("\n").forEach((line, lnum) => {
137
- this.line_num = lnum;
138
-
139
- /* if line looks like a title */
140
- const titleMatch = line.trim().match(/^(#+) (.+)$/);
141
-
142
- if (titleMatch) {
143
- if (this.opts.verbose || this.opts.debug)
144
- console.log("found toc element: " + line);
145
-
146
- /* implement toc level */
147
- let level = titleMatch[1].length;
148
- let title = titleMatch[2];
149
-
150
- this.opts.secs.push({ level, title });
151
-
152
- if (this.opts.debug) {
153
- console.log("updated sections:", { level, title });
154
- }
155
- }
156
- });
157
-
158
- return this.parse_commands(blob, commands.parse);
159
- }
160
-
161
- preprocess(blob: string) {
162
- if (this.opts.verbose || this.opts.debug) {
163
- console.debug(`beginning preprocess of '${this.file}'`.blue);
164
- }
165
-
166
- return this.parse_commands(blob, commands.preparse);
167
- }
168
-
169
- postprocess(blob: string) {
170
- if (this.opts.verbose || this.opts.debug) {
171
- console.debug(`beginning postprocess of '${this.file}'`.blue);
172
- }
173
-
174
- blob = this.parse_commands(blob, commands.postparse);
175
-
176
- /* remove double empty lines */
177
- blob = this.remove_double_blank_lines(blob);
178
- blob = blob.trimEnd() + "\n\n";
179
- return blob;
180
- }
181
-
182
- parse_commands(blob: string, commands: Command[]) {
183
- commands.forEach((command) => {
184
- /* Add global flag to RegExp */
185
- const re = new RegExp(
186
- command.validator.source,
187
- (command.validator.flags || "") + "g"
188
- );
189
- blob = blob.replace(re, (...args) => command.act(args, this) || "");
190
- });
191
- return blob;
192
- }
193
-
194
- parse_all_commands(blob: string, commands: { [key: string]: Command[] }) {
195
- Object.keys(commands).forEach((key) => {
196
- blob = this.parse_commands(blob, commands[key]);
197
- });
198
- return blob;
199
- }
200
-
201
- titleId(title: string) {
202
- const sep = this.opts.use_underscore ? "_" : "-";
203
-
204
- title = title
205
- .toLowerCase()
206
- .replace(/[^\w\s]+/g, "")
207
- .replace(/[\s_]+/g, sep);
208
- return title;
209
- }
210
-
211
- gen_toc() {
212
- let __blob = [];
213
- let tabSize = 2;
214
- const beg = "* ";
215
- const hor = " ".repeat(tabSize);
216
-
217
- this.opts.secs.forEach((sec) => {
218
- if (sec.level > this.opts.toc_level) return;
219
- let title = sec.title.replace(/_/g, " ");
220
- title = this.parse_all_commands(title, commands);
221
- const link = this.titleId(title);
222
-
223
- let __line =
224
- hor.repeat(Math.max(sec.level - 1, 0)) +
225
- beg +
226
- `[${title}](#${link})`;
227
-
228
- __blob.push(__line);
229
- });
230
- return __blob.join("\n");
231
- }
232
-
233
- add_hook(name: string, hook: () => string) {
234
- if (this.opts.hooks[name] != undefined)
235
- throw new Error(`Hook ${name} already exists!`);
236
- this.opts.hooks[name] = hook;
237
- }
238
-
239
- add_adv_hook(name: string, hook: (tree: HTMLElement) => HTMLElement) {
240
- if (this.opts.hooks[name] != undefined)
241
- throw new Error(`Hook ${name} already exists!`);
242
- this.opts.adv_hooks[name] = hook;
243
- }
244
-
245
- line_num_from_index(index: number) {
246
- return this.raw.substring(0, index).split("\n").length + 1;
247
- }
248
-
249
- remove_double_blank_lines(blob) {
250
- /* replace all triple newlines, and EOF by double newline */
251
- blob = blob.replace(/(\r\n|\n){3,}/g, "\n\n");
252
-
253
- return blob;
254
- }
255
-
256
- /* output the parsed document to bundle */
257
- to(bundleName: string, callback: (fileName: string) => void) {
258
- const dir = path.dirname(bundleName);
259
- if (callback === undefined) callback = () => {};
260
-
261
- if (!fs.existsSync(dir)) {
262
- fs.mkdirSync(dir, { recursive: true });
263
- }
264
-
265
- if (!this.opts.html) {
266
- this.get(TargetType.MARKDOWN, (blob) => {
267
- fs.writeFile(bundleName, blob, () => callback(bundleName));
268
- });
269
- } else {
270
- const htmlFileName = bundleName.replace(".md", ".html");
271
- fs.writeFile(htmlFileName, this.html(), () =>
272
- callback(htmlFileName)
273
- );
274
- }
275
- }
276
-
277
- html() {
278
- const htmlFormatted = marked(this.get(TargetType.HTML));
279
- if (this.opts.watch) {
280
- return (
281
- `<script>w=new WebSocket("ws:localhost:7788");w.addEventListener("message",(e)=>{if(e.data=="refresh")location.reload();});</script>\n` +
282
- htmlFormatted
283
- );
284
- }
285
- return htmlFormatted;
286
- }
287
-
288
- get(targetType?: TargetType, callback?: (blob: string) => void): string {
289
- /* If target type is undefined, markdown is the default */
290
- if (targetType === undefined) targetType = TargetType.MARKDOWN;
291
- if (this.blobs[targetType]) {
292
- if (callback) {
293
- callback(this.blobs[targetType]);
294
- }
295
- return this.blobs[targetType];
296
- } else {
297
- try {
298
- this.opts.targetType = targetType;
299
- let blob = this.parse();
300
- this.opts.targetType = undefined;
301
- if (callback) callback(blob);
302
- return blob;
303
- } catch (error) {
304
- /* Compile a traceback of error */
305
- let traceback = "";
306
- let p: Parser = this;
307
-
308
- do {
309
- if (error instanceof MDMError)
310
- traceback += `\n...on line ${p.line_num_from_index(
311
- error.match.index
312
- )} in ${p.file}`.grey(15);
313
- else
314
- traceback +=
315
- `\n...on line ${p.line_num} in ${p.file}`.grey(15);
316
- if (p.parent) p = p.parent;
317
- } while (p.parent);
318
-
319
- error.message += traceback;
320
-
321
- /* only interested in node stacktrace when debugging */
322
- if (!this.opts.debug) error.stack = "";
323
-
324
- if (this.opts.only_warn) console.error(error);
325
- else throw error;
326
- }
327
- }
328
- }
19
+ file: string;
20
+ parent?: Parser;
21
+ line_num: number;
22
+ wd: string;
23
+ wd_full: string;
24
+ blobs: {
25
+ [key: number]: string | undefined;
26
+ };
27
+ opts: ParserOptions;
28
+ raw: string;
29
+
30
+ static TOKEN = "#md";
31
+
32
+ constructor(
33
+ filename: string,
34
+ clargs?: IncompleteCommandLineArgs,
35
+ opts?: IncompleteParserOptions
36
+ ) {
37
+ /* this.working_directory */
38
+ this.file = filename;
39
+
40
+ this.line_num = 0;
41
+ this.wd = path.dirname(filename);
42
+ this.wd_full = path.resolve(this.wd);
43
+
44
+ /* finished blob */
45
+ this.blobs = {};
46
+
47
+ if (!clargs) {
48
+ clargs = argParser.parse_args([filename]);
49
+ }
50
+
51
+ /* get default options, and overwrite with the ones present
52
+ in the arguments */
53
+ this.opts = defaultParserOptions();
54
+ Object.assign(this.opts, clargs);
55
+ Object.assign(this.opts, opts);
56
+
57
+ this.raw = this.opts.isFileCallback(filename) || filename;
58
+ }
59
+
60
+ private parse() {
61
+ load_extensions(this);
62
+ if (this.opts.verbose || this.opts.debug) {
63
+ console.log(
64
+ `parsing ${this.file}: depth=${this.opts.depth}`.magenta
65
+ );
66
+ }
67
+
68
+ if (this.opts.debug) {
69
+ console.log("Parsing options:");
70
+ console.log(this.opts);
71
+ }
72
+
73
+ /* reset sections for beginning parse */
74
+ if (this.opts.depth === 0) this.opts.secs = [];
75
+ let __blob = this.raw;
76
+
77
+ /* apply preproccessing to raw file */
78
+ __blob = this.preprocess(__blob);
79
+
80
+ /* main parser instance call */
81
+ __blob = this.mainparse(__blob);
82
+
83
+ /**
84
+ * apply postprocessing after */
85
+ __blob = this.postprocess(__blob);
86
+
87
+ return __blob;
88
+ }
89
+
90
+ private mainparse(blob: string) {
91
+ if (this.opts.verbose || this.opts.debug) {
92
+ console.debug(`beginning mainparse of '${this.file}'`.blue);
93
+ }
94
+
95
+ /* main parser instance loop */
96
+ blob.split("\n").forEach((line, lnum) => {
97
+ this.line_num = lnum;
98
+
99
+ /* if line looks like a title */
100
+ const titleMatch = line.trim().match(/^(#+) (.+)$/);
101
+
102
+ if (titleMatch) {
103
+ if (this.opts.verbose || this.opts.debug)
104
+ console.log("found toc element: " + line);
105
+
106
+ /* implement toc level */
107
+ let level = titleMatch[1].length;
108
+ let title = titleMatch[2];
109
+
110
+ this.opts.secs.push({ level, title });
111
+
112
+ if (this.opts.debug) {
113
+ console.log("updated sections:", { level, title });
114
+ }
115
+ }
116
+ });
117
+
118
+ return this.parse_commands(blob, commands.parse);
119
+ }
120
+
121
+ private preprocess(blob: string) {
122
+ if (this.opts.verbose || this.opts.debug) {
123
+ console.debug(`beginning preprocess of '${this.file}'`.blue);
124
+ }
125
+
126
+ return this.parse_commands(blob, commands.preparse);
127
+ }
128
+
129
+ private postprocess(blob: string) {
130
+ if (this.opts.verbose || this.opts.debug) {
131
+ console.debug(`beginning postprocess of '${this.file}'`.blue);
132
+ }
133
+
134
+ blob = this.parse_commands(blob, commands.postparse);
135
+
136
+ /* remove double empty lines */
137
+ blob = this.remove_double_blank_lines(blob);
138
+ blob = blob.trimEnd() + "\n\n";
139
+ return blob;
140
+ }
141
+
142
+ private parse_commands(blob: string, commands: Command[]) {
143
+ commands.forEach((command) => {
144
+ /* Add global flag to RegExp */
145
+ const re = new RegExp(
146
+ command.validator.source,
147
+ (command.validator.flags || "") + "g"
148
+ );
149
+
150
+ const replacer = (args: RegExpExecArray) => {
151
+ try {
152
+ return command.act(args, this) || "";
153
+ } catch (error) {
154
+ switch (true) {
155
+ case error instanceof MDMError:
156
+ throw error;
157
+ case error instanceof MDMWarning:
158
+ console.warn(error.message);
159
+ return `**Warning: ${error.message}**`;
160
+ default:
161
+ console.error(error);
162
+ throw error;
163
+ }
164
+ }
165
+ };
166
+
167
+ /* */
168
+
169
+ let match: RegExpExecArray | null;
170
+ while ((match = re.exec(blob)) !== null) {
171
+ blob =
172
+ blob.slice(0, match.index) +
173
+ replacer(match) +
174
+ blob.slice(match.index + match[0].length);
175
+ }
176
+ });
177
+ return blob;
178
+ }
179
+
180
+ /* Parse all commands sequentially on a sub-blob */
181
+ private parse_all_commands(blob: string, commands: CommandGroupType) {
182
+ blob = this.parse_commands(blob, commands.preparse);
183
+ blob = this.parse_commands(blob, commands.parse);
184
+ blob = this.parse_commands(blob, commands.postparse);
185
+ return blob;
186
+ }
187
+
188
+ titleId(title: string) {
189
+ const sep = this.opts.use_underscore ? "_" : "-";
190
+
191
+ title = title
192
+ .toLowerCase()
193
+ .replace(/[^\w\s]+/g, "")
194
+ .replace(/[\s_]+/g, sep);
195
+ return title;
196
+ }
197
+
198
+ get_toc() {
199
+ let __blob = [];
200
+ let tabSize = 2;
201
+ const beg = "* ";
202
+ const hor = " ".repeat(tabSize);
203
+
204
+ this.opts.secs.forEach((sec) => {
205
+ if (sec.level > this.opts.toc_level) return;
206
+ let title = sec.title.replace(/_/g, " ");
207
+ title = this.parse_all_commands(title, commands);
208
+ const link = this.titleId(title);
209
+
210
+ let __line =
211
+ hor.repeat(Math.max(sec.level - 1, 0)) +
212
+ beg +
213
+ `[${title}](#${link})`;
214
+
215
+ __blob.push(__line);
216
+ });
217
+ return __blob.join("\n");
218
+ }
219
+
220
+ add_hook(
221
+ name: string,
222
+ hook: (map: { [key: string]: TaggedElement }) => void
223
+ ) {
224
+ if (this.opts.hooks[name] != undefined)
225
+ throw new MDMNonParserError(`Hook "${name}" already exists!`);
226
+ this.opts.hooks[name] = hook;
227
+ }
228
+
229
+ private line_num_from_index(index: number) {
230
+ return this.raw.substring(0, index).split("\n").length;
231
+ }
232
+
233
+ private remove_double_blank_lines(blob) {
234
+ /* replace all triple newlines, and EOF by double newline */
235
+ blob = blob.replace(/(\r\n|\n){3,}/g, "\n\n");
236
+
237
+ return blob;
238
+ }
239
+
240
+ /* output the parsed document to bundle */
241
+ to(bundleName: string, callback: (fileName: string) => void) {
242
+ const dir = path.dirname(bundleName);
243
+ if (callback === undefined) callback = () => {};
244
+
245
+ if (!fs.existsSync(dir)) {
246
+ fs.mkdirSync(dir, { recursive: true });
247
+ }
248
+
249
+ if (!this.opts.html) {
250
+ this.get(TargetType.MARKDOWN, (blob) => {
251
+ fs.writeFile(bundleName, blob, () => callback(bundleName));
252
+ });
253
+ } else {
254
+ const htmlFileName = bundleName.replace(".md", ".html");
255
+ fs.writeFile(htmlFileName, this.html(), () =>
256
+ callback(htmlFileName)
257
+ );
258
+ }
259
+ }
260
+
261
+ html() {
262
+ const htmlFormatted = marked
263
+ .parse(this.get(TargetType.HTML))
264
+ .toString();
265
+ if (this.opts.watch) {
266
+ return (
267
+ `<script>` +
268
+ `w=new WebSocket("ws:localhost:7788");` +
269
+ `w.addEventListener("message",(e)=>` +
270
+ ` {if(e.data=="refresh")location.reload();}` +
271
+ `);` +
272
+ `</script>\n` +
273
+ htmlFormatted
274
+ );
275
+ }
276
+ return htmlFormatted;
277
+ }
278
+
279
+ createChild(file: string) {
280
+ return new Parser(file, undefined, {
281
+ parent: this,
282
+ depth: this.opts.depth + 1,
283
+ ...this.opts,
284
+ });
285
+ }
286
+
287
+ get(targetType?: TargetType, callback?: (blob: string) => void): string {
288
+ /* If target type is undefined, markdown is the default */
289
+ if (targetType === undefined) targetType = TargetType.MARKDOWN;
290
+ if (this.blobs[targetType]) {
291
+ if (callback) {
292
+ callback(this.blobs[targetType]);
293
+ }
294
+ return this.blobs[targetType];
295
+ } else {
296
+ try {
297
+ this.opts.targetType = targetType;
298
+ let blob = this.parse();
299
+ this.opts.targetType = undefined;
300
+ if (callback) callback(blob);
301
+ return blob;
302
+ } catch (error) {
303
+ /* Compile a traceback of error */
304
+ let traceback = "";
305
+ let p: Parser = this;
306
+
307
+ do {
308
+ if (error instanceof MDMError)
309
+ traceback += `\n...on line ${p.line_num_from_index(
310
+ error.match.index
311
+ )} in ${p.file}`.grey(15);
312
+ else
313
+ traceback +=
314
+ `\n...on line ${p.line_num} in ${p.file}`.grey(15);
315
+ if (p.parent) p = p.parent;
316
+ } while (p.parent);
317
+
318
+ error.message += traceback;
319
+
320
+ /* only interested in node stacktrace when debugging */
321
+ if (!this.opts.debug) error.stack = "";
322
+
323
+ if (this.opts.only_warn) console.error(error);
324
+ else throw error;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ function defaultParserOptions(): ParserOptions {
331
+ return {
332
+ defs: {},
333
+ secs: [],
334
+ args: [],
335
+ depth: 0,
336
+ verbose: false,
337
+ debug: false,
338
+ max_depth: 5,
339
+ use_underscore: false,
340
+ toc_level: 3,
341
+ allow_undefined: false,
342
+ html: false,
343
+ watch: false,
344
+ targetType: undefined,
345
+ only_warn: false,
346
+ parent: undefined,
347
+ hooks: {},
348
+ isFileCallback: (f) => {
349
+ if (!fs.existsSync(f)) return false;
350
+ return fs.readFileSync(f, "utf-8") + "\n";
351
+ },
352
+ };
329
353
  }
330
354
 
331
355
  export function splice(
332
- str: string,
333
- startIndex: number,
334
- width: number,
335
- newSubStr: string
356
+ str: string,
357
+ startIndex: number,
358
+ width: number,
359
+ newSubStr: string
336
360
  ) {
337
- const start = str.slice(0, startIndex);
338
- const end = str.slice(startIndex + width);
339
- return start + newSubStr + end;
361
+ const start = str.slice(0, startIndex);
362
+ const end = str.slice(startIndex + width);
363
+ return start + newSubStr + end;
340
364
  }
341
365
 
342
366
  /* add extention to marked for classed blockquotes*/
343
367
  marked.use({
344
- renderer: {
345
- blockquote(quote) {
346
- /* find the ending, and if not, return the default */
347
- const ending = quote.match(/\{(.+)\}\s*<\/p>/);
348
- if (!ending) return `<blockquote>${quote}</blockquote>`;
349
-
350
- const args = ending[1].split(" ");
351
-
352
- const classes = args.filter((arg) => arg.startsWith("."));
353
- const id = args.filter((arg) => arg.startsWith("#"));
354
-
355
- const classNames = classes.map((c) => c.slice(1));
356
- const classText =
357
- classes.length > 0 ? `class="${classNames.join(" ")}"` : "";
358
- const idText = id.length > 0 ? `id="${id[0].slice(1)}"` : "";
359
-
360
- /* remove the ending from the quote */
361
- quote = quote.replace(/\{(.+)\}\s*<\/p>/, "</p>");
362
-
363
- return `<blockquote ${classText} ${idText}>\n${quote.trim()}</blockquote>`;
364
- },
365
- },
368
+ renderer: {
369
+ blockquote(quote: string) {
370
+ /* find the ending, and if not, return the default */
371
+ const ending = quote.match(/\{(.+)\}\s*<\/p>/);
372
+ if (!ending) return `<blockquote>${quote}</blockquote>`;
373
+
374
+ const args = ending[1].split(" ");
375
+
376
+ const classes = args.filter((arg) => arg.startsWith("."));
377
+ const id = args.filter((arg) => arg.startsWith("#"));
378
+
379
+ const classNames = classes.map((c) => c.slice(1));
380
+ const classText =
381
+ classes.length > 0 ? `class="${classNames.join(" ")}"` : "";
382
+ const idText = id.length > 0 ? `id="${id[0].slice(1)}"` : "";
383
+
384
+ /* remove the ending from the quote */
385
+ quote = quote.replace(/\{(.+)\}\s*<\/p>/, "</p>");
386
+
387
+ return `<blockquote ${classText} ${idText}>\n${quote.trim()}</blockquote>`;
388
+ },
389
+ heading(text: string, level: number) {
390
+ /* add an id to each heading */
391
+ return `<h${level} id="${text
392
+ .replace(/ /g, "-")
393
+ .toLowerCase()}">${text}</h${level}>`;
394
+ },
395
+ },
366
396
  });
367
397
 
368
- module.exports = Parser;
369
-
370
398
  export default Parser;