@linkeddatanotes/md2rdf 0.0.0-stage → 0.0.5

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,789 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DateTimeHierarchy = exports.Md2Rdf = exports.TimeNoDateInHeadingHierarchyWarning = exports.HomonymHeadingWarning = exports.ParseError = void 0;
4
+ exports.commonNamespaces = commonNamespaces;
5
+ // Explicitly importing Parser not from 'n3', see src/n3-lib.d.ts.
6
+ const N3Parser_1 = require("n3/lib/N3Parser");
7
+ const namespace_1 = require("@rdfjs/namespace");
8
+ const fs_1 = require("fs");
9
+ const path_1 = require("path");
10
+ /**
11
+ * ParseError is passed in the onError() handler of Md2RdfParserOptions.
12
+ * Implementations of the Md2RdfParserOptions.onError() handler should read the ParseError parameter
13
+ * for logging and deciding whether to continue.
14
+ */
15
+ class ParseError extends Error {
16
+ /**
17
+ * file IRI of the currently parsed Markdown file.
18
+ */
19
+ baseIRI;
20
+ /**
21
+ * severity level
22
+ *
23
+ * error - data cannot be parsed
24
+ * warning - data is parsed but result may be unexpected
25
+ */
26
+ severity;
27
+ /**
28
+ * Line number within the currently parsed Markdown file.
29
+ */
30
+ line;
31
+ /**
32
+ * create a ParseError.
33
+ * Helper function
34
+ * @param cause Underlying error or message.
35
+ * @param options Options to pass the baseIRI of the currently parsed Markdown file.
36
+ * @param line Line number within the currently parsed Markdown file.
37
+ * @param severity Severity level 'error' or 'warning' inspired by methods of `console.`.
38
+ * If severity is not passed, assume 'error' unless the cause Error name ends with string 'Warning'.
39
+ * @returns
40
+ */
41
+ constructor(cause, options, line, severity) {
42
+ // if severity is not passed, assume error unless the cause Error name ends with string 'Warning'
43
+ if (severity === undefined)
44
+ severity =
45
+ cause instanceof Error && cause.name.endsWith('Warning')
46
+ ? 'warning'
47
+ : 'error';
48
+ super(
49
+ // severity: transform the first character to uppercase to have a nicely readable
50
+ // Warning or Error in the message string.
51
+ 'Md2Rdf: ' +
52
+ severity[0].toUpperCase() +
53
+ severity.slice(1) +
54
+ ' parsing file ' +
55
+ options.baseIRI +
56
+ (line ? ' line ' + line : '') +
57
+ ': ' +
58
+ (cause instanceof Error ? cause.message : String(cause)), { cause: cause });
59
+ this.name = 'ParseError';
60
+ this.severity = severity;
61
+ this.baseIRI = options.baseIRI;
62
+ this.line = line;
63
+ }
64
+ }
65
+ exports.ParseError = ParseError;
66
+ /**
67
+ * HomonymHeadingWarning is a possible cause in ParseError.
68
+ * Example:
69
+ * Heading "MyHeading" used for different sections containing a turtle block with "this:". IRI "file#MyHeading" is homonym for both sections.
70
+ */
71
+ class HomonymHeadingWarning extends Error {
72
+ name = 'HomonymHeadingWarning';
73
+ }
74
+ exports.HomonymHeadingWarning = HomonymHeadingWarning;
75
+ /**
76
+ * TimeNoDateInHeadingHierarchyWarning is a possible cause in ParseError.
77
+ * Example:
78
+ * Heading with time "13:45" could be nested within a date to define schema:temporalCoverage. No parent date found.
79
+ */
80
+ class TimeNoDateInHeadingHierarchyWarning extends Error {
81
+ name = 'TimeNoDateInHeadingHierarchyWarning';
82
+ }
83
+ exports.TimeNoDateInHeadingHierarchyWarning = TimeNoDateInHeadingHierarchyWarning;
84
+ /**
85
+ * Md2Rdf parses Markdown files and returns RDF quads.
86
+ *
87
+ * It follows the minimalistic LinkedDataNotes convention.
88
+ *
89
+ * It makes a "best effort" attempt to extract RDF from Markdown. It will report ParseError instances and go on if not stopped.
90
+ * Example: Syntax errors in one turtle code fence break this one fence, but the data in the next turtle code fences may be fine.
91
+ *
92
+ * Documentation: see README of the project.
93
+ *
94
+ */
95
+ class Md2Rdf {
96
+ options;
97
+ optionsNamespaceLineCount = 0;
98
+ // temporary objects and helpers that only exist during a parse operation, shared between objects
99
+ context;
100
+ /**
101
+ * parserN3 is loaded lazily using _parserN3() when a Markdown file actually contains `turtle`.
102
+ */
103
+ parserN3;
104
+ constructor(options) {
105
+ this.options = options;
106
+ if (this.options.namespaces) {
107
+ // ensure namespaces ends with a newline
108
+ if (!this.options.namespaces.endsWith('\n'))
109
+ this.options.namespaces = this.options.namespaces + '\n';
110
+ this.optionsNamespaceLineCount =
111
+ this.options.namespaces.split('\n').length - 1;
112
+ }
113
+ }
114
+ /**
115
+ * only instantiate the parser when we really need it, to save memory and performance when checking many files
116
+ * @returns lazy-loaded parser
117
+ */
118
+ _parserN3() {
119
+ return (this.parserN3 ??= new N3Parser_1.default({
120
+ ...this.options,
121
+ format: 'text/n3',
122
+ }));
123
+ }
124
+ /**
125
+ * Parse the line.
126
+ * If it is an [atx heading](https://spec.commonmark.org/0.31.2/#atx-heading),
127
+ * 1. remember it in this.headings
128
+ * 2. generate quads that result from the heading, if it contains information according to LinkedDataNotes convention
129
+ * @param line
130
+ * @returns true, when this is a heading
131
+ */
132
+ _parseHeading(line) {
133
+ if (!this.context)
134
+ throw new Error('ParseContext missing');
135
+ // shortcuts
136
+ const c = this.context;
137
+ // parse the line, is it a heading?
138
+ if (c.headings.parseLine(line, this.context.byLine.linenumber ?? 0)) {
139
+ const dateTime = c.headings.dateTimeHierarchy?.toISODateTimeString(this.context);
140
+ // there is a 'this:' in the rdfBuffer and a dateTime in the current dateTimeHierarchy.
141
+ // therefore, add the date to the rdf of this block
142
+ if (dateTime) {
143
+ // shortcuts
144
+ const f = this.options.factory;
145
+ const ns = c.ns;
146
+ const thisSectionIRINode = c.headings.prepThisSectionIRI(c);
147
+ if (!thisSectionIRINode) {
148
+ throw new ParseError('Programming Bug: _parseHeading found heading but prepThisSectionIRI() returns unknown. Line "' +
149
+ line +
150
+ "'", this.options, c.byLine.linenumber);
151
+ }
152
+ // add temporalCoverage quad
153
+ c.quads.push(f.quad(thisSectionIRINode, ns.schema.temporalCoverage, f.literal(dateTime)));
154
+ }
155
+ return true;
156
+ }
157
+ else {
158
+ return false;
159
+ }
160
+ }
161
+ /**
162
+ * Parse turtle or N3 syntax from the passed code block from the markdown file
163
+ * @param {String} n3 - the turtle passed as String
164
+ * @param {number} startLine the line number where the block started
165
+ * @return quads the quads parsed from the turtleString as array
166
+ * @throws error , see N3 parse()
167
+ */
168
+ _parseN3String(n3, startLine) {
169
+ try {
170
+ // Only a shallow evaluation, no full parsing: does the n3 contain a "this:" ?
171
+ // A proper implementation would implement a handler that is called when N3 Parser/Lexer
172
+ // identifies a namespace prefix and only then activates the "this:" convention.
173
+ const containsThisMarker = n3.includes('this:');
174
+ const thisIRINode = containsThisMarker
175
+ ? this.context?.headings.prepThisSectionIRI(this.context)
176
+ : undefined;
177
+ const s =
178
+ // the default namespaces to prepend before any n3 so authors can implicitly rely on them when writing blocks
179
+ (this.options.namespaces ? this.options.namespaces : '') +
180
+ // "this:"" is a shortcut identifying the current section fragmet id in the current markdown file
181
+ // note to self: '== null' checks both null AND undefined, whereas '=== null' only checks null
182
+ (thisIRINode === undefined
183
+ ? ''
184
+ : 'PREFIX this: <' + thisIRINode.value + '>\n') +
185
+ n3;
186
+ let quads = undefined;
187
+ try {
188
+ quads = this._parserN3().parse(s);
189
+ }
190
+ catch (e) {
191
+ // catch and handle N3 block parsing error
192
+ if (e instanceof Error) {
193
+ // replace the line numbering,
194
+ e.message = e.message.replace(/ on line (\d+)/, (_match, lineStr) => ' on line ' +
195
+ (Number(lineStr) -
196
+ // reducing it by the artificially added optionsNamespaceLineCount
197
+ (this.optionsNamespaceLineCount +
198
+ // and the "PREFIX this:"" line, if it has been added
199
+ (thisIRINode === undefined ? 0 : 1))));
200
+ }
201
+ throw e;
202
+ }
203
+ // append parsed quads to result by mutating target.
204
+ // Intentionally not using the call-stack intensive alternative "this.context?.quads.push(...quads);"
205
+ Array.prototype.push.apply(this.context?.quads, quads);
206
+ }
207
+ catch (e) {
208
+ if (this.options.onError) {
209
+ // wrap it in a ParseError?
210
+ const pe = e instanceof ParseError
211
+ ? e
212
+ : new ParseError(e, this.options, startLine + 1);
213
+ // call handler
214
+ this.options.onError(pe);
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Check if the current line starts with a turtleBlock.
220
+ * If yes, parse the contents of a turtle code fence block and process it
221
+ * @param line - the line to check if it is an opening ```turtle code fence block
222
+ * @returns true, if this line started a turtle block
223
+ */
224
+ _parseTurtleBlock(line) {
225
+ // opening fence: up to 3 spaces indent, 3+ backticks or tildes (not mixed), info string turtle
226
+ // note: the (turtle) could be extended to (turtle|ttl|n3|) if we evolve the LinkedDataNotes convention.
227
+ const openMatch = line.match(/^ {0,3}(`{3,}|~{3,})\s*(turtle)\s*$/);
228
+ if (!openMatch)
229
+ // if no openMatch, return as quickly as possible
230
+ return false;
231
+ // ok, start parsing in detail
232
+ if (!this.context)
233
+ throw new Error('ParseContext missing');
234
+ // closing fence: same character type, at least as many characters, up to 3 spaces indent, only trailing whitespace
235
+ const fenceChar = openMatch[1][0] === '`' ? '`' : '~';
236
+ const closingFence = new RegExp('^ {0,3}' + fenceChar + '{' + openMatch[1].length + ',}\\s*$');
237
+ const startLine = this.context.byLine.linenumber;
238
+ let rdfBuffer = '';
239
+ let blockClosed = false;
240
+ for (let next = this.context.byLine.next(); !next.done; next = this.context.byLine.next()) {
241
+ if (closingFence.test(next.value)) {
242
+ blockClosed = true;
243
+ break;
244
+ }
245
+ else {
246
+ rdfBuffer += ('' === rdfBuffer ? '' : '\n') + next.value;
247
+ }
248
+ }
249
+ // was the block properly closed?
250
+ if (!blockClosed && this.options.onError) {
251
+ // handler may throw the passed exception back at us here
252
+ this.options.onError(new ParseError('Block starting at line ' + startLine + ' was never closed', this.options, startLine));
253
+ }
254
+ // we have a block of turtle string in the rdfBuffer, parse it
255
+ this._parseN3String(rdfBuffer, startLine);
256
+ return true;
257
+ }
258
+ /**
259
+ * Parses markdown and returns an array of quads.
260
+ * See README.md
261
+ *
262
+ * Note: the method must only be called by a single thread. To parallelize parsing, use two Md2Rdf instances.
263
+ *
264
+ * @param input string of markdown
265
+ * @returns
266
+ * quads: the array of rdf.js Quad parsed so far
267
+ * error: this is the error that stopped the run, it should have been thrown by options.onError.
268
+ * @throws errors when there are unexpected programming bugs
269
+ */
270
+ parseString(input) {
271
+ if (this.context)
272
+ throw new Error('parseString() was called while parsing already in progress. Create multiple instances of Md2Rdf for parallel processing.');
273
+ // initialize parsing by line
274
+ const byLine = new StringByLine(input);
275
+ // context for this parsing run, will be reset at the end of this function
276
+ this.context = new ParseContext(this.options, byLine);
277
+ // initialize variables for return values of this function at the end
278
+ let error = undefined;
279
+ let quads;
280
+ try {
281
+ for (let next = byLine.next(); !next.done; next = byLine.next()) {
282
+ const line = next.value;
283
+ // evaluate in the order of specificality/priority
284
+ // 1. is the line a markdown atx heading? https://spec.commonmark.org/0.31.2/#atx-heading
285
+ // using a logic expression without storing its return value.
286
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
287
+ this._parseHeading(line) ||
288
+ // 2. is the line starting a code fence? https://spec.commonmark.org/0.31.2/#code-fence
289
+ this._parseTurtleBlock(line);
290
+ }
291
+ }
292
+ catch (errorThrown) {
293
+ if (errorThrown instanceof ParseError)
294
+ // the options.onError handler threw a ParseError that had been passed to it. This signals the parser to stop and return the result.
295
+ error = errorThrown;
296
+ else
297
+ // "this should not happen" (tm)
298
+ throw errorThrown;
299
+ }
300
+ finally {
301
+ // remember the result before deleting the temporary objects
302
+ quads = this.context.quads ?? [];
303
+ // reset temporary objects to undefined, to ensure the parallel processing warning will work
304
+ this.context = undefined;
305
+ }
306
+ return { quads, error };
307
+ }
308
+ }
309
+ exports.Md2Rdf = Md2Rdf;
310
+ //////////////////////////////
311
+ // Internal helper Classes
312
+ // not exported
313
+ //////////////////////////////
314
+ /**
315
+ * Helper Object to
316
+ * - hold variables during parsing
317
+ * - share helpers between Md2Rdf and Headings2This
318
+ */
319
+ class ParseContext {
320
+ options;
321
+ // Namespaces
322
+ ns;
323
+ quads;
324
+ byLine;
325
+ headings;
326
+ constructor(options, byLine) {
327
+ this.options = options;
328
+ this.byLine = byLine;
329
+ // initialize temporary objects shared by the parse functions
330
+ this.quads = [];
331
+ // helper for headings
332
+ this.headings = new Headings2This(this.options);
333
+ // namespace constants to write classes and properties.
334
+ // note: must be tied to the same datafactory as Md2Rdf
335
+ const o = { factory: this.options.factory };
336
+ this.ns = {
337
+ rdf: (0, namespace_1.default)('http://www.w3.org/1999/02/22-rdf-syntax-ns#', o),
338
+ rdfs: (0, namespace_1.default)('http://www.w3.org/2000/01/rdf-schema#', o),
339
+ schema: (0, namespace_1.default)('https://schema.org/', o),
340
+ md2rdf: (0, namespace_1.default)('https://www.linkeddatanotes.org/ns/md2rdf/', o),
341
+ };
342
+ }
343
+ }
344
+ /**
345
+ * A generator that takes the passed string,
346
+ * splits it by \r\n and \n,
347
+ * yield each line,
348
+ * count current linenumber.
349
+ *
350
+ * Does not split everything in memory in the beginning to save memory.
351
+ *
352
+ * Use as iterator:
353
+ * let by = new StringByLine('example');
354
+ * for (const line of by) {
355
+ * console.log(line);
356
+ * }
357
+ *
358
+ * Use with linenumber: see below in parseString()
359
+ *
360
+ * @param input string
361
+ */
362
+ class StringByLine {
363
+ input;
364
+ linenumber = 0;
365
+ start = 0;
366
+ idx;
367
+ constructor(input) {
368
+ this.input = input;
369
+ }
370
+ next() {
371
+ if (this.idx === -1)
372
+ return { value: undefined, done: true };
373
+ this.idx = this.input.indexOf('\n', this.start);
374
+ if (this.idx !== -1) {
375
+ this.linenumber++;
376
+ const s = this.input.substring(this.start, this.idx > 0 && this.input[this.idx - 1] === '\r'
377
+ ? this.idx - 1
378
+ : this.idx);
379
+ this.start = this.idx + 1;
380
+ return { value: s, done: false };
381
+ }
382
+ return { value: this.input.substring(this.start), done: false };
383
+ }
384
+ [Symbol.iterator]() {
385
+ return this;
386
+ }
387
+ }
388
+ /**
389
+ * Helper class to concatenate ISO Times from filename and headings.
390
+ *
391
+ * The first element in the DateTimeHierarchy is the current heading (or filename, if no section).
392
+ * It points to a parent element in DateTimeHierarchy for further information.
393
+ *
394
+ * To read it, call toISODateTimeString.
395
+ *
396
+ * Once a date or date+time is found in the time hierarchy, it is returned.
397
+ * If no date is found, undefined is returned.
398
+ *
399
+ * Updating the current DateTimeHierarchy while parsing Markdown is handled by update()
400
+ *
401
+ * Evaluation is lazy: the current hierarchy is stored after every heading,
402
+ * but the regex time parsing is only done when an actual this: element is found in a turtle block
403
+ * to save CPU.
404
+ *
405
+ * Assumptions:
406
+ * - vast majority of Markdown will not contain turtle and `this:`, only seldomly date will be needed
407
+ * - regex parsing costs CPU
408
+ */
409
+ class DateTimeHierarchy {
410
+ /**
411
+ * This should match a DATE followed by TIME followed by ZONE
412
+ * DATE is optional
413
+ * TIME is optional, if TIME is set, ZONE is also optional
414
+ * yyyy-mm-dd ... optional
415
+ * 13:55
416
+ * 2021-02-10
417
+ * 2021-02-10 13:55
418
+ * 2021-02-10T13:55
419
+ * 2021-02-10 13:55:44.1234
420
+ * 2021-02-10T13:55Z+01:00
421
+ */
422
+ static DATE_TIME_ZONE = /^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)(?:Z?([+-]\d{2}:\d{2})|Z)?)?/;
423
+ static TIME_ONLY = /^(\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)(?:Z?([+-]\d{2}:\d{2})|Z)?/;
424
+ /**
425
+ * Same as DATE_TIME_ZONE, but the date part allows reduced ISO 8601 precision
426
+ * (yyyy or yyyy-mm, not just yyyy-mm-dd), and is anchored at both ends since it
427
+ * is only ever run against a single endpoint token already isolated by DURATION.
428
+ */
429
+ static DATE_TIME_ZONE_FLEX = /^(\d{4}(?:-\d{2}(?:-\d{2})?)?)(?:[T ](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)(?:Z?([+-]\d{2}:\d{2})|Z)?)?$/;
430
+ /**
431
+ * Matches a schema:temporalCoverage-style duration: two date/time endpoints
432
+ * separated by "/", "-" or "–" (an ISO 8601 time interval, e.g. "1939/1945" or
433
+ * "2015-11/.."). The end endpoint may be ".." (open-ended), a date/time of its
434
+ * own, or a bare time that shares the start endpoint's date.
435
+ */
436
+ static DURATION = /^(\d{4}(?:-\d{2}(?:-\d{2})?)?(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z?[+-]\d{2}:\d{2}|Z)?)?)\s*(?:\/|-|–)\s*(\.\.|\d{4}(?:-\d{2}(?:-\d{2})?)?(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z?[+-]\d{2}:\d{2}|Z)?)?|\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z?[+-]\d{2}:\d{2}|Z)?)/;
437
+ parent;
438
+ /**
439
+ * 0 = filename from baseIRI
440
+ * 1 = h1
441
+ * ...
442
+ * 6 = h6
443
+ */
444
+ level;
445
+ /**
446
+ * the content of the heading (after the #)
447
+ * or
448
+ * the filename
449
+ */
450
+ input;
451
+ /**
452
+ * was the input already parsed to date/time/zone?
453
+ * for lazy evaluation
454
+ */
455
+ parsed = false;
456
+ date;
457
+ time;
458
+ zone;
459
+ /**
460
+ * The following three fields are only set when the input is a duration
461
+ * (ISO 8601 time interval, e.g. "1939/1945" or "2015-11/..").
462
+ * endDate/endTime/endZone hold the end endpoint; when the end endpoint is a
463
+ * bare time (e.g. "09:00-17:00"), endDate stays undefined and the start's
464
+ * own date is reused for it.
465
+ */
466
+ endDate;
467
+ endTime;
468
+ endZone;
469
+ /**
470
+ * true when the duration's end endpoint was written as ".." (open-ended,
471
+ * e.g. "2015-11/..").
472
+ */
473
+ openEnded;
474
+ constructor(level, input, parent) {
475
+ this.level = level;
476
+ this.input = input;
477
+ this.parent = parent;
478
+ }
479
+ /**
480
+ * In the hierarchy, find the next date.
481
+ * @returns a date if found, as string.
482
+ */
483
+ _findDate() {
484
+ this._parse();
485
+ if (this.date) {
486
+ return this.date;
487
+ }
488
+ return this.parent ? this.parent._findDate() : undefined;
489
+ }
490
+ /**
491
+ * Ensure this heading has been parsed before accessing the fields.
492
+ * Read the input, find a date and time, write it into the fields.
493
+ */
494
+ _parse() {
495
+ if (this.parsed)
496
+ return;
497
+ // try duration first: e.g. "2021-02-10" alone must not be shadowed by "2021-02"/"10"
498
+ const durationMatch = DateTimeHierarchy.DURATION.exec(this.input);
499
+ if (durationMatch !== null) {
500
+ const startMatch = DateTimeHierarchy.DATE_TIME_ZONE_FLEX.exec(durationMatch[1]);
501
+ if (startMatch !== null) {
502
+ this.date = startMatch[1];
503
+ this.time = startMatch[2];
504
+ this.zone = startMatch[3];
505
+ }
506
+ if (durationMatch[2] === '..') {
507
+ this.openEnded = true;
508
+ }
509
+ else {
510
+ const endMatch = DateTimeHierarchy.DATE_TIME_ZONE_FLEX.exec(durationMatch[2]);
511
+ if (endMatch !== null) {
512
+ this.endDate = endMatch[1];
513
+ this.endTime = endMatch[2];
514
+ this.endZone = endMatch[3];
515
+ }
516
+ else {
517
+ // end endpoint has no date of its own (e.g. "17:00"), reuse the start's date
518
+ const endTimeMatch = DateTimeHierarchy.TIME_ONLY.exec(durationMatch[2]);
519
+ if (endTimeMatch !== null) {
520
+ this.endTime = endTimeMatch[1];
521
+ this.endZone = endTimeMatch[2];
522
+ }
523
+ }
524
+ }
525
+ this.parsed = true;
526
+ return;
527
+ }
528
+ const match = DateTimeHierarchy.DATE_TIME_ZONE.exec(this.input);
529
+ if (match !== null) {
530
+ this.date = match[1];
531
+ this.time = match[2];
532
+ this.zone = match[3];
533
+ }
534
+ else {
535
+ const timeMatch = DateTimeHierarchy.TIME_ONLY.exec(this.input);
536
+ if (timeMatch !== null) {
537
+ this.time = timeMatch[1];
538
+ this.zone = timeMatch[2];
539
+ }
540
+ }
541
+ this.parsed = true;
542
+ }
543
+ /**
544
+ * true when this heading's input was parsed as a duration (ISO 8601 time
545
+ * interval), i.e. it has an end endpoint or is open-ended.
546
+ */
547
+ get isDuration() {
548
+ this._parse();
549
+ return (this.openEnded === true ||
550
+ this.endDate !== undefined ||
551
+ this.endTime !== undefined);
552
+ }
553
+ /**
554
+ * Read the current Heading Hierarchy and format it as a schema:temporalCoverage value:
555
+ * either a plain ISO 8601 date/date-time, or - when the heading was written as a
556
+ * duration - an ISO 8601 time interval such as "1939/1945" or "2015-11/..".
557
+ * @returns the ISO 8601 date/date-time or interval defined by the heading hierarchy, or undefined if none found.
558
+ * @throws DoubleTimeInHeadingHierarchyWarning, TimeNoDateInHeadingHierarchyWarning
559
+ */
560
+ toISODateTimeString(context) {
561
+ this._parse();
562
+ if (this.isDuration) {
563
+ // this.date is always set for a duration: DURATION's start endpoint always includes a year.
564
+ const start = this.date + (this.time ? 'T' + this.time + (this.zone ?? '') : '');
565
+ const end = this.openEnded
566
+ ? '..'
567
+ : this.endDate !== undefined
568
+ ? this.endDate +
569
+ (this.endTime ? 'T' + this.endTime + (this.endZone ?? '') : '')
570
+ : this.date + 'T' + this.endTime + (this.endZone ?? '');
571
+ return start + '/' + end;
572
+ }
573
+ if (this.date !== undefined) {
574
+ // this is a date, return it
575
+ return this.date + (this.time ? 'T' + this.time + (this.zone ?? '') : '');
576
+ }
577
+ else {
578
+ if (this.time !== undefined) {
579
+ // there is a time, look for a "date" in the parents
580
+ const parentDate = this.parent ? this.parent._findDate() : undefined;
581
+ if (parentDate !== undefined) {
582
+ return (parentDate + (this.time ? 'T' + this.time + (this.zone ?? '') : ''));
583
+ }
584
+ else {
585
+ if (context.options.onError) {
586
+ context.options.onError(new ParseError(new TimeNoDateInHeadingHierarchyWarning('Heading with time "' +
587
+ this.time +
588
+ '" could be nested within a date to define schema:temporalCoverage. No parent date found.'), context.options, context.byLine.linenumber));
589
+ }
590
+ return undefined; // TimeNoDateInHeadingHierarchyWarning
591
+ }
592
+ }
593
+ else {
594
+ // no time
595
+ return undefined;
596
+ }
597
+ }
598
+ }
599
+ /**
600
+ * update the DateTimeHierarchy based on the latest input
601
+ * @param level current level
602
+ * @param input content of the heading, after "#"
603
+ * @param parent parent in the DateTimeHierarchy
604
+ * @returns an update pointer to the current relevant heading or file in the DateTimeHierarchy
605
+ */
606
+ static update(level, input, parent) {
607
+ if (parent === undefined) {
608
+ if (level !== 0)
609
+ throw new Error('DateTimeHierarchy: when initializing with an empty parent, the level must be 0');
610
+ return new DateTimeHierarchy(level, input);
611
+ }
612
+ else {
613
+ // there is a parent
614
+ if (!(level >= 1))
615
+ throw new Error('DateTimeHierarchy: updating a hierarchy, and there is a parent, level for a new heading must be >= 1. Level: ' +
616
+ level);
617
+ while (parent && parent.level >= level && parent.parent) {
618
+ // the new input is working with the parent of this parent
619
+ parent = parent.parent;
620
+ }
621
+ return new DateTimeHierarchy(level, input, parent);
622
+ }
623
+ }
624
+ }
625
+ exports.DateTimeHierarchy = DateTimeHierarchy;
626
+ /**
627
+ * cache for loadCommonNamespaces
628
+ */
629
+ let cachedCommonNamespaces;
630
+ /**
631
+ * Lazily load and cache the commonNamespaces.ttl shipped with md2rdf.
632
+ * @returns the common namespaces as turtle string
633
+ */
634
+ function commonNamespaces() {
635
+ return (cachedCommonNamespaces ??= (0, fs_1.readFileSync)((0, path_1.join)(__dirname, 'commonNamespaces.ttl'), 'utf-8'));
636
+ }
637
+ /**
638
+ * Handling of headings and `this:`
639
+ */
640
+ class Headings2This {
641
+ options;
642
+ // Parameters in the order they build upon each other
643
+ /**
644
+ * reusable regex to match headings
645
+ */
646
+ matchHeading = /^(#{1,6}) (.*)/;
647
+ /**
648
+ * Buffer for the last heading content that was passed to parseLine,
649
+ * to be used when needed by prepThisSectionIRI.
650
+ * It is the "content" after the hash-marks and the space
651
+ * https://spec.commonmark.org/0.31.2/#atx-headings
652
+ */
653
+ lastHeadingContent;
654
+ /**
655
+ * line number of the lastHeadingContent
656
+ */
657
+ lastHeadingLinenumber;
658
+ /**
659
+ * The URI for the current section after the lastHeadingContent.
660
+ * Initiated undefined and reset to undefined after each heading, initialized lazily when needed.
661
+ */
662
+ thisSectionIRI;
663
+ /**
664
+ * multiple headings having the same hash?
665
+ * remember them to send a warning
666
+ */
667
+ usedThisIRIs = new Set();
668
+ /**
669
+ * Time and date data found in the hierarchy of
670
+ * - filename (baseIRI) and
671
+ * - headings 1 to 6
672
+ */
673
+ dateTimeHierarchy;
674
+ /**
675
+ * Passing options
676
+ * @param options
677
+ */
678
+ constructor(options) {
679
+ this.options = options;
680
+ // the slash / in the group [] doesnt need to be escaped
681
+ let filename = options.baseIRI.replace(/^.*[/:]/, '');
682
+ filename = decodeURI(filename); // %20 -> ' ', etc.
683
+ this.dateTimeHierarchy = DateTimeHierarchy.update(0, filename);
684
+ }
685
+ /**
686
+ * Translate thea markdown heading to a fragment which can be used in an IRI.
687
+ * See LinkeDataNotes-Markdown Syntax
688
+ * @param {String} headingContent the content of the heading. This is the string after the ATX heading hash-marks and space (not including leading hash-signs). https://spec.commonmark.org/0.31.2/#atx-headings
689
+ * @return {String} the fragment
690
+ */
691
+ _headingToFragment(headingContent) {
692
+ // replace all(=g) space by -
693
+ let fragment = headingContent.replace(/ /g, '-');
694
+ // encode non-URI chars
695
+ fragment = encodeURIComponent(fragment);
696
+ return fragment;
697
+ }
698
+ /**
699
+ * create a section IRI based on file and fragment
700
+ */
701
+ _createSectionIri(inputfileIRI, fragment) {
702
+ return inputfileIRI + '#' + fragment;
703
+ }
704
+ /**
705
+ * Parse the line for a heading starting with `#`, see [atx heading](https://spec.commonmark.org/0.31.2/#atx-heading).
706
+ * It remembers the headings that were passed to it, should they be later needed by prepThisSectionIRI() or the dateTimeHierarchy.
707
+ * @param line the Markdown line
708
+ * @param linenumber the current line number, should it be needed for error logging later
709
+ * @returns true, when the current line is an atx heading starting with '#'
710
+ */
711
+ parseLine(line, linenumber) {
712
+ // is it a heading?
713
+ const match = this.matchHeading.exec(line);
714
+ if (match) {
715
+ // remember the heading
716
+ // trim() because [atx heading](https://spec.commonmark.org/0.31.2/#atx-heading) says:
717
+ // "Leading and trailing spaces or tabs are ignored in parsing inline content"
718
+ this.lastHeadingContent = match[2].trim();
719
+ // reset this.thisSectionIRI as mark for prepThisSectionIRI later that it creates the quads only once
720
+ this.thisSectionIRI = undefined;
721
+ this.lastHeadingLinenumber = linenumber;
722
+ // remember hierarchy from Filename down to Headings
723
+ const level = match[1].length; // the group in the regex matching the ###
724
+ this.dateTimeHierarchy = DateTimeHierarchy.update(level, this.lastHeadingContent, this.dateTimeHierarchy);
725
+ return true;
726
+ }
727
+ else {
728
+ return false;
729
+ }
730
+ }
731
+ /**
732
+ * Prepare the current section IRI, as the caller will use it in quads.
733
+ *
734
+ * This:
735
+ * - mints the IRI, remembers it as node for reuse further down the section
736
+ * - adds metadata about the section via quads.push()
737
+ *
738
+ * @returns the section IRI as RDF Node or undefined if no heading was in the file yet
739
+ */
740
+ prepThisSectionIRI(context) {
741
+ // return the cached result, if it exists
742
+ if (this.thisSectionIRI) {
743
+ return this.thisSectionIRI;
744
+ }
745
+ else {
746
+ // prep thisSectionIRI and keep it to reuse it
747
+ // Syntax: this is only supported after the first heading
748
+ if (this.lastHeadingContent === undefined) {
749
+ // leave thisSectionIRI as undefined
750
+ return undefined;
751
+ }
752
+ // identify thisSectionIRI based on last heading
753
+ const fragment = this._headingToFragment(this.lastHeadingContent);
754
+ const thisSectionIRIString = this._createSectionIri(this.options.baseIRI, fragment);
755
+ // check if thisSectionIRI already appeared in a previous section
756
+ const firstUse = !this.usedThisIRIs.has(fragment);
757
+ if (!firstUse) {
758
+ // onError Handler set?
759
+ if (this.options.onError)
760
+ // call it!
761
+ this.options.onError(new ParseError(new HomonymHeadingWarning('Heading "' +
762
+ this.lastHeadingContent +
763
+ '" used for different sections containing a turtle block with "this:". IRI "' +
764
+ thisSectionIRIString +
765
+ '" is homonym for both sections.'), this.options, this.lastHeadingLinenumber));
766
+ }
767
+ else
768
+ this.usedThisIRIs.add(fragment);
769
+ // shortcuts
770
+ const f = context.options.factory;
771
+ const ns = context.ns;
772
+ // create the Node
773
+ this.thisSectionIRI = f.namedNode(thisSectionIRIString);
774
+ if (firstUse) {
775
+ // first use, add annotations
776
+ context.quads.push(
777
+ // type
778
+ f.quad(this.thisSectionIRI, ns.rdf.type, ns.md2rdf.Section),
779
+ // label
780
+ f.quad(this.thisSectionIRI, ns.rdfs.label, f.literal(this.lastHeadingContent)),
781
+ // headingLevel
782
+ f.quad(this.thisSectionIRI, ns.md2rdf.headingLevel, f.literal((this.dateTimeHierarchy?.level ?? 0).toString())));
783
+ }
784
+ // return it
785
+ return this.thisSectionIRI;
786
+ }
787
+ }
788
+ }
789
+ //# sourceMappingURL=Md2Rdf.js.map