md-processor 1.0.0

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,21 @@
1
+ name: Test
2
+ on:
3
+ - pull_request
4
+ - push
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-20.04
8
+ strategy:
9
+ matrix:
10
+ node:
11
+ - '16'
12
+ - '18'
13
+ - '20'
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - uses: actions/setup-node@v3
17
+ with:
18
+ node-version: ${{ matrix.node }}
19
+ - run: npm install
20
+ - run: npm test
21
+ - uses: codecov/codecov-action@v3
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Thomas Bergwinkl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # md-processor
2
+
3
+ [![build status](https://img.shields.io/github/actions/workflow/status/bergos/md-processor/test.yaml?branch=master)](https://github.com/bergos/md-processor/actions/workflows/test.yaml)
4
+ [![npm version](https://img.shields.io/npm/v/md-processor.svg)](https://www.npmjs.com/package/md-processor)
5
+
6
+ A preprocessor for markdown files.
7
+
8
+ ## Usage
9
+
10
+ You can run `md-processor` with `npx` like shown below:
11
+
12
+ ```bash
13
+ nxp md-processor input.md > output.md
14
+ ```
15
+
16
+ ## Features
17
+
18
+ ### Imports
19
+
20
+ Sections of other markdown files can be imported like this:
21
+
22
+ ```markdown
23
+ @[import{section}](filename)
24
+ ```
25
+
26
+ - `section`: The full header of the section to import
27
+ - `filename`: The path to the markdown file to import.
28
+ The path is relative to the input file.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import Processor from '../lib/Processor.js'
5
+
6
+ const program = new Command()
7
+
8
+ program
9
+ .argument('<input>', 'markdown to process')
10
+ .action(async input => {
11
+ const processor = new Processor()
12
+ const result = await processor.process(input)
13
+
14
+ process.stdout.write(result)
15
+ })
16
+
17
+ program.parse(process.argv)
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import Processor from './lib/Processor.js'
2
+
3
+ async function main () {
4
+ const processor = new Processor()
5
+
6
+ console.log(await processor.process('../../rdf-ext/rdf-ext.org/template/index.md'))
7
+ }
8
+
9
+ main()
package/lib/Block.js ADDED
@@ -0,0 +1,17 @@
1
+ class Block {
2
+ constructor ({ header = null, level = 0 } = {}) {
3
+ this.header = header
4
+ this.level = level
5
+ this.lines = []
6
+ }
7
+
8
+ append (line) {
9
+ this.lines.push(line)
10
+ }
11
+
12
+ toString () {
13
+ return this.lines.join('\n')
14
+ }
15
+ }
16
+
17
+ export default Block
@@ -0,0 +1,55 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import Block from './Block.js'
3
+
4
+ const parseHeaderRegex = /(#+) (.*)/
5
+
6
+ class Markdown {
7
+ constructor () {
8
+ this.blocks = []
9
+ }
10
+
11
+ get isEmpty () {
12
+ return this.blocks.length === 0
13
+ }
14
+
15
+ get last () {
16
+ return this.blocks[this.blocks.length - 1]
17
+ }
18
+
19
+ append ({ header, level } = {}) {
20
+ this.blocks.push(new Block({ header, level }))
21
+
22
+ return this
23
+ }
24
+
25
+ get (header) {
26
+ return this.blocks.find(block => block.header === header)
27
+ }
28
+
29
+ parse (content) {
30
+ const lines = content.split('\n')
31
+
32
+ for (const line of lines) {
33
+ const parsed = line.match(parseHeaderRegex)
34
+
35
+ if (parsed) {
36
+ this.append({
37
+ header: parsed[2],
38
+ level: parsed[1]
39
+ })
40
+ }
41
+
42
+ this.last.append(line)
43
+ }
44
+
45
+ return this
46
+ }
47
+
48
+ static async load (path) {
49
+ const content = (await readFile(path)).toString()
50
+
51
+ return new Markdown().parse(content)
52
+ }
53
+ }
54
+
55
+ export default Markdown
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { dirname, resolve } from 'node:path'
3
+ import Markdown from './Markdown.js'
4
+
5
+ const parseImportRegex = /@\[import{([^}]*)}]\(([^)]*)\)/g
6
+
7
+ class Processor {
8
+ constructor ({ path } = {}) {
9
+ this.path = path || process.cwd()
10
+ }
11
+
12
+ async process (path) {
13
+ path = resolve(this.path, path)
14
+
15
+ let content = (await readFile(path)).toString()
16
+
17
+ content = await this.processImports({ content, path })
18
+
19
+ return content
20
+ }
21
+
22
+ async processImports ({ content, path }) {
23
+ const imports = new Map()
24
+
25
+ for (const [text, header, importPath] of [...content.matchAll(parseImportRegex)]) {
26
+ imports.set(text, { header, importPath })
27
+ }
28
+
29
+ for (const [text, { header, importPath }] of imports) {
30
+ const resolved = resolve(dirname(path), importPath)
31
+ const markdown = await Markdown.load(resolved)
32
+
33
+ content = content.replaceAll(text, markdown.get(header))
34
+ }
35
+
36
+ return content
37
+ }
38
+ }
39
+
40
+ export default Processor
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "md-processor",
3
+ "version": "1.0.0",
4
+ "description": "A preprocessor for markdown files",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "md-processor": "bin/md-processor.js"
9
+ },
10
+ "scripts": {
11
+ "test": "stricter-standard && c8 --reporter=lcov --reporter=text-summary mocha"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/bergos/md-processor.git"
16
+ },
17
+ "keywords": [],
18
+ "author": "Thomas Bergwinkl <bergi@axolotlfarm.org> (https://www.bergnet.org/people/bergi/card#me)",
19
+ "license": "MIT",
20
+ "bugs": {
21
+ "url": "https://github.com/bergos/md-processor/issues"
22
+ },
23
+ "homepage": "https://github.com/bergos/md-processor",
24
+ "dependencies": {
25
+ "commander": "^10.0.1"
26
+ },
27
+ "devDependencies": {
28
+ "c8": "^7.14.0",
29
+ "mocha": "^10.2.0",
30
+ "stricter-standard": "^0.2.0"
31
+ }
32
+ }
@@ -0,0 +1,60 @@
1
+ import { strictEqual } from 'node:assert'
2
+ import { describe, it } from 'mocha'
3
+ import Block from '../lib/Block.js'
4
+
5
+ describe('Block', () => {
6
+ it('should be a constructor', () => {
7
+ strictEqual(typeof Block, 'function')
8
+ })
9
+
10
+ it('should assign the given header argument', () => {
11
+ const header = {}
12
+ const block = new Block({ header })
13
+
14
+ strictEqual(block.header, header)
15
+ })
16
+
17
+ it('should assign the given level argument', () => {
18
+ const level = {}
19
+ const block = new Block({ level })
20
+
21
+ strictEqual(block.level, level)
22
+ })
23
+
24
+ it('should have an empty array property named lines', () => {
25
+ const block = new Block()
26
+
27
+ strictEqual(Array.isArray(block.lines), true)
28
+ strictEqual(block.lines.length, 0)
29
+ })
30
+
31
+ describe('.append', () => {
32
+ it('should be a method', () => {
33
+ const block = new Block()
34
+
35
+ strictEqual(typeof block.append, 'function')
36
+ })
37
+
38
+ it('should append the given line', () => {
39
+ const line = 'test'
40
+ const block = new Block()
41
+
42
+ block.append(line)
43
+
44
+ strictEqual(block.lines.length, 1)
45
+ strictEqual(block.lines[0], line)
46
+ })
47
+ })
48
+
49
+ describe('.toString', () => {
50
+ it('should return all lines joined by a line break', () => {
51
+ const lines = ['abc', 'def']
52
+ const block = new Block()
53
+
54
+ block.append(lines[0])
55
+ block.append(lines[1])
56
+
57
+ strictEqual(block.toString(), lines.join('\n'))
58
+ })
59
+ })
60
+ })
@@ -0,0 +1,133 @@
1
+ import { deepStrictEqual, strictEqual } from 'node:assert'
2
+ import { describe, it } from 'mocha'
3
+ import Markdown from '../lib/Markdown.js'
4
+ import * as examples from './support/examples.js'
5
+
6
+ describe('Markdown', () => {
7
+ it('should be a constructor', () => {
8
+ strictEqual(typeof Markdown, 'function')
9
+ })
10
+
11
+ it('should have an empty array property named blocks', () => {
12
+ const markdown = new Markdown()
13
+
14
+ strictEqual(Array.isArray(markdown.blocks), true)
15
+ strictEqual(markdown.blocks.length, 0)
16
+ })
17
+
18
+ describe('.isEmpty', () => {
19
+ it('should be a boolean property', () => {
20
+ const markdown = new Markdown()
21
+
22
+ strictEqual(typeof markdown.isEmpty, 'boolean')
23
+ })
24
+ })
25
+
26
+ describe('.last', () => {
27
+ it('should be a property pointing to the last block', () => {
28
+ const markdown = new Markdown().append({}).append({})
29
+
30
+ strictEqual(markdown.last, markdown.blocks[1])
31
+ })
32
+
33
+ it('should be undefined if the object is empty', () => {
34
+ const markdown = new Markdown()
35
+
36
+ strictEqual(typeof markdown.last, 'undefined')
37
+ })
38
+ })
39
+
40
+ describe('.append', () => {
41
+ it('should be a method', () => {
42
+ const markdown = new Markdown()
43
+
44
+ strictEqual(typeof markdown.append, 'function')
45
+ })
46
+
47
+ it('should append a new block with the given header and level', () => {
48
+ const header = 'test'
49
+ const level = '###'
50
+ const markdown = new Markdown()
51
+
52
+ markdown.append({ header, level })
53
+
54
+ strictEqual(markdown.blocks.length, 1)
55
+ strictEqual(markdown.blocks[0].header, header)
56
+ strictEqual(markdown.blocks[0].level, level)
57
+ })
58
+
59
+ it('should return this', () => {
60
+ const markdown = new Markdown()
61
+
62
+ const result = markdown.append({})
63
+
64
+ strictEqual(result, markdown)
65
+ })
66
+ })
67
+
68
+ describe('.get', () => {
69
+ it('should be a method', () => {
70
+ const markdown = new Markdown()
71
+
72
+ strictEqual(typeof markdown.get, 'function')
73
+ })
74
+
75
+ it('should return the block with the matching header', () => {
76
+ const markdown = new Markdown()
77
+ .append({ header: 'abc' })
78
+ .append({ header: 'def' })
79
+
80
+ const result = markdown.get('def')
81
+
82
+ strictEqual(result.header, 'def')
83
+ })
84
+
85
+ it('should return undefined if no matching block was found', () => {
86
+ const markdown = new Markdown()
87
+
88
+ const result = markdown.get('def')
89
+
90
+ strictEqual(typeof result, 'undefined')
91
+ })
92
+ })
93
+
94
+ describe('.parse', () => {
95
+ it('should be a method', () => {
96
+ const markdown = new Markdown()
97
+
98
+ strictEqual(typeof markdown.parse, 'function')
99
+ })
100
+
101
+ it('should parse the given content', () => {
102
+ const markdown = new Markdown()
103
+
104
+ markdown.parse(examples.multiBlock.content)
105
+
106
+ strictEqual(markdown.blocks[0].header, examples.multiBlock.blocks[0].header)
107
+ strictEqual(markdown.blocks[0].level, examples.multiBlock.blocks[0].level)
108
+ deepStrictEqual(markdown.blocks[0].lines, examples.multiBlock.blocks[0].lines)
109
+
110
+ strictEqual(markdown.blocks[1].header, examples.multiBlock.blocks[1].header)
111
+ strictEqual(markdown.blocks[1].level, examples.multiBlock.blocks[1].level)
112
+ deepStrictEqual(markdown.blocks[1].lines, examples.multiBlock.blocks[1].lines)
113
+ })
114
+ })
115
+
116
+ describe('static .load', () => {
117
+ it('should be a static method', () => {
118
+ strictEqual(typeof Markdown.load, 'function')
119
+ })
120
+
121
+ it('should parse the given file', async () => {
122
+ const markdown = await Markdown.load('./test/support/multiBlock.md')
123
+
124
+ strictEqual(markdown.blocks[0].header, examples.multiBlock.blocks[0].header)
125
+ strictEqual(markdown.blocks[0].level, examples.multiBlock.blocks[0].level)
126
+ deepStrictEqual(markdown.blocks[0].lines, examples.multiBlock.blocks[0].lines)
127
+
128
+ strictEqual(markdown.blocks[1].header, examples.multiBlock.blocks[1].header)
129
+ strictEqual(markdown.blocks[1].level, examples.multiBlock.blocks[1].level)
130
+ deepStrictEqual(markdown.blocks[1].lines, examples.multiBlock.blocks[1].lines)
131
+ })
132
+ })
133
+ })
@@ -0,0 +1,39 @@
1
+ import { strictEqual } from 'node:assert'
2
+ import { describe, it } from 'mocha'
3
+ import Processor from '../lib/Processor.js'
4
+ import * as examples from './support/examples.js'
5
+
6
+ describe('Processor', () => {
7
+ it('should be a constructor', () => {
8
+ strictEqual(typeof Processor, 'function')
9
+ })
10
+
11
+ it('should assign the given path argument', () => {
12
+ const path = {}
13
+ const processor = new Processor({ path })
14
+
15
+ strictEqual(processor.path, path)
16
+ })
17
+
18
+ it('should assign process.cwd() to the path property if no argument is given', () => {
19
+ const processor = new Processor()
20
+
21
+ strictEqual(processor.path, process.cwd())
22
+ })
23
+
24
+ describe('.process', () => {
25
+ it('should be a method', () => {
26
+ const processor = new Processor()
27
+
28
+ strictEqual(typeof processor.process, 'function')
29
+ })
30
+
31
+ it('should process all imports', async () => {
32
+ const processor = new Processor()
33
+
34
+ const result = await processor.process('./test/support/multiImports.md')
35
+
36
+ strictEqual(result, examples.multiImports.content)
37
+ })
38
+ })
39
+ })
@@ -0,0 +1,64 @@
1
+ const multiBlock = (() => {
2
+ const blocks = [{
3
+ header: 'Header 1',
4
+ level: '#',
5
+ lines: [
6
+ '# Header 1',
7
+ 'First line of the body of header 1',
8
+ 'Second line of the body of header 1'
9
+ ]
10
+ }, {
11
+ header: 'Header 1.1',
12
+ level: '##',
13
+ lines: [
14
+ '## Header 1.1',
15
+ 'First line of the body of header 1.1',
16
+ 'Second line of the body of header 1.1'
17
+ ]
18
+ }]
19
+
20
+ return {
21
+ blocks,
22
+ content: [
23
+ blocks[0].lines.join('\n'),
24
+ blocks[1].lines.join('\n')
25
+ ].join('\n')
26
+ }
27
+ })()
28
+
29
+ const multiImports = (() => {
30
+ const blocks = [
31
+ ...multiBlock.blocks,
32
+ {
33
+ header: 'Header 2',
34
+ level: '#',
35
+ lines: [
36
+ '# Header 2',
37
+ 'First line of the body of header 2',
38
+ 'Second line of the body of header 2'
39
+ ]
40
+ }, {
41
+ header: 'Header 2.1',
42
+ level: '##',
43
+ lines: [
44
+ '## Header 2.1',
45
+ 'First line of the body of header 2.1',
46
+ 'Second line of the body of header 2.1'
47
+ ]
48
+ }]
49
+
50
+ return {
51
+ blocks,
52
+ content: [
53
+ blocks[0].lines.join('\n'),
54
+ blocks[1].lines.join('\n'),
55
+ blocks[2].lines.join('\n'),
56
+ blocks[3].lines.join('\n')
57
+ ].join('\n')
58
+ }
59
+ })()
60
+
61
+ export {
62
+ multiBlock,
63
+ multiImports
64
+ }
@@ -0,0 +1,6 @@
1
+ # Header 1
2
+ First line of the body of header 1
3
+ Second line of the body of header 1
4
+ ## Header 1.1
5
+ First line of the body of header 1.1
6
+ Second line of the body of header 1.1
@@ -0,0 +1,8 @@
1
+ @[import{Header 1}](multiBlock.md)
2
+ @[import{Header 1.1}](multiBlock.md)
3
+ # Header 2
4
+ First line of the body of header 2
5
+ Second line of the body of header 2
6
+ ## Header 2.1
7
+ First line of the body of header 2.1
8
+ Second line of the body of header 2.1