markshell 1.6.0 → 1.8.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.
- package/bin/markshell.js +45 -0
- package/docs/usage.md +22 -4
- package/lib/admonitions/index.js +18 -15
- package/lib/index.js +63 -13
- package/lib/syntaxhighlighter/index.d.ts +31 -1
- package/lib/syntaxhighlighter/index.js +180 -33
- package/package.json +15 -8
package/bin/markshell.js
CHANGED
|
@@ -36,6 +36,51 @@ if (args.length === 0 || args.indexOf('--help') !== -1) {
|
|
|
36
36
|
printHelp();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Handle --example flag to run interactive demos
|
|
40
|
+
const exampleIdx = args.indexOf('--example');
|
|
41
|
+
if (exampleIdx !== -1) {
|
|
42
|
+
const path = require('path');
|
|
43
|
+
const exampleName = args[exampleIdx + 1];
|
|
44
|
+
|
|
45
|
+
const examples = {
|
|
46
|
+
'dynamic-loading': path.join(__dirname, '../examples/dynamic-loading-demo.js'),
|
|
47
|
+
'syntax-colors': path.join(__dirname, '../examples/syntax-colors-demo.js'),
|
|
48
|
+
'demo': path.join(__dirname, '../DEMO.md')
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (!exampleName || !examples[exampleName]) {
|
|
52
|
+
console.log(`Markshell - Version ${pkg.version}
|
|
53
|
+
|
|
54
|
+
${chalk.yellow.bold('Available examples:')}
|
|
55
|
+
|
|
56
|
+
${chalk.cyan('markshell --example dynamic-loading')}
|
|
57
|
+
Interactive demo showing dynamic language loading with statistics
|
|
58
|
+
|
|
59
|
+
${chalk.cyan('markshell --example syntax-colors')}
|
|
60
|
+
Visual demonstration of syntax highlighting colors
|
|
61
|
+
|
|
62
|
+
${chalk.cyan('markshell --example demo')}
|
|
63
|
+
Comprehensive demo showcasing all markdown features and 25+ languages
|
|
64
|
+
`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const examplePath = examples[exampleName];
|
|
69
|
+
|
|
70
|
+
if (exampleName === 'demo') {
|
|
71
|
+
// Render the DEMO.md file
|
|
72
|
+
try {
|
|
73
|
+
markshell.toConsole(examplePath);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
printError(e);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// Execute the example script
|
|
79
|
+
require(examplePath);
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
39
84
|
const theme = args.indexOf('--theme');
|
|
40
85
|
|
|
41
86
|
if (theme !== 1) {
|
package/docs/usage.md
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
# Usage
|
|
2
2
|
|
|
3
3
|
```bash
|
|
4
|
-
markshell [
|
|
4
|
+
markshell [filepath | -f [filepath] | --filepath [filepath]]
|
|
5
|
+
markshell --example [example-name]
|
|
6
|
+
markshell --help
|
|
5
7
|
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Options
|
|
10
|
+
|
|
11
|
+
**filepath or -f**
|
|
8
12
|
: path to markdown file
|
|
13
|
+
|
|
14
|
+
**--example [name]**
|
|
15
|
+
: run interactive examples and demos
|
|
16
|
+
- `dynamic-loading` - Shows dynamic language loading with statistics
|
|
17
|
+
- `syntax-colors` - Visual demonstration of syntax highlighting colors
|
|
18
|
+
- `demo` - Comprehensive showcase of all markdown features
|
|
19
|
+
|
|
9
20
|
**--help**
|
|
10
|
-
:
|
|
21
|
+
: display usage information
|
|
11
22
|
|
|
12
23
|
## Examples
|
|
13
24
|
|
|
14
25
|
```sh
|
|
26
|
+
# Render a markdown file
|
|
27
|
+
markshell ./my/markdownfile.md
|
|
15
28
|
markshell --filepath './my/markdownfile.md'
|
|
16
|
-
|
|
29
|
+
|
|
30
|
+
# Run interactive examples
|
|
31
|
+
markshell --example dynamic-loading
|
|
32
|
+
markshell --example syntax-colors
|
|
33
|
+
markshell --example demo
|
|
34
|
+
```
|
package/lib/admonitions/index.js
CHANGED
|
@@ -95,21 +95,23 @@ const offset = () => {
|
|
|
95
95
|
|
|
96
96
|
let offsetLength;
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
// console.log(typeof this.styles.indent.afterIndent, this.styles.indent.afterIndent);
|
|
100
|
-
// console.log(typeof this.styles.indent.titleIndent, this.styles.indent.titleIndent);
|
|
98
|
+
const currentStyles = styles || formatBlock;
|
|
101
99
|
|
|
102
|
-
|
|
100
|
+
// console.log(typeof currentStyles.indent.beforeIndent, currentStyles.indent.beforeIndent);
|
|
101
|
+
// console.log(typeof currentStyles.indent.afterIndent, currentStyles.indent.afterIndent);
|
|
102
|
+
// console.log(typeof currentStyles.indent.titleIndent, currentStyles.indent.titleIndent);
|
|
103
|
+
|
|
104
|
+
offsetLength = (currentStyles.indent.beforeIndent) + 2 + currentStyles.indent.afterIndent + currentStyles.indent.titleIndent;
|
|
103
105
|
|
|
104
106
|
// console.log(process.stdout.columns < offsetLength, offsetLength);
|
|
105
107
|
|
|
106
108
|
if (process.stdout.columns < offsetLength) {
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
currentStyles.indent.beforeIndent = defBeforeIndent;
|
|
111
|
+
currentStyles.indent.afterIndent = defAfterIndent;
|
|
112
|
+
currentStyles.indent.titleIndent = defTitleIndent;
|
|
111
113
|
|
|
112
|
-
offsetLength =
|
|
114
|
+
offsetLength = currentStyles.indent.beforeIndent + 2 + currentStyles.indent.afterIndent + currentStyles.indent.titleIndent;
|
|
113
115
|
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -119,18 +121,19 @@ const offset = () => {
|
|
|
119
121
|
|
|
120
122
|
const formatOutput = (title, style) => {
|
|
121
123
|
|
|
124
|
+
const currentStyles = styles || formatBlock;
|
|
122
125
|
let indentOffset = offset();
|
|
123
126
|
let outTitle = " " + title + " "
|
|
124
127
|
|
|
125
|
-
if (
|
|
128
|
+
if (currentStyles.indent.beforeIndent === 0 && currentStyles.indent.titleIndent === 0) {
|
|
126
129
|
outTitle = title + " ";
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
return " ".repeat(
|
|
130
|
-
style(" ".repeat(
|
|
132
|
+
return " ".repeat(currentStyles.indent.beforeIndent) +
|
|
133
|
+
style(" ".repeat(currentStyles.indent.titleIndent)) +
|
|
131
134
|
chalk.bgHex('#000').white(outTitle) +
|
|
132
135
|
style(" ".repeat(process.stdout.columns - indentOffset - title.length)) +
|
|
133
|
-
chalk.bgHex('#000')(" ".repeat(
|
|
136
|
+
chalk.bgHex('#000')(" ".repeat(currentStyles.indent.afterIndent)) +
|
|
134
137
|
EOL;
|
|
135
138
|
|
|
136
139
|
}
|
|
@@ -226,13 +229,13 @@ const _getBlocks = (content, beforeIndent, afterIndent, titleIndent, useSafeStyl
|
|
|
226
229
|
*/
|
|
227
230
|
const _getStyles = () => {
|
|
228
231
|
|
|
229
|
-
if (
|
|
232
|
+
if (styles === null || styles === undefined) {
|
|
230
233
|
|
|
231
234
|
return formatBlock;
|
|
232
235
|
|
|
233
236
|
} else {
|
|
234
237
|
|
|
235
|
-
return
|
|
238
|
+
return styles;
|
|
236
239
|
|
|
237
240
|
}
|
|
238
241
|
|
|
@@ -240,7 +243,7 @@ const _getStyles = () => {
|
|
|
240
243
|
|
|
241
244
|
const _setStyles = (styleBlock) => {
|
|
242
245
|
|
|
243
|
-
|
|
246
|
+
styles = styleBlock;
|
|
244
247
|
|
|
245
248
|
}
|
|
246
249
|
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,15 @@ const syntaxHighlighter = require('./syntaxhighlighter');
|
|
|
7
7
|
const admonitions = require('./admonitions');
|
|
8
8
|
const EOL = require('os').EOL;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Escape special regex characters to prevent ReDoS attacks
|
|
12
|
+
* @param {string} str - String to escape
|
|
13
|
+
* @returns {string} - Escaped string safe for use in RegExp
|
|
14
|
+
*/
|
|
15
|
+
const escapeRegex = (str) => {
|
|
16
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
};
|
|
18
|
+
|
|
10
19
|
const theme = require('./syntaxhighlighter/themes/okaidia.theme');
|
|
11
20
|
let _theme;
|
|
12
21
|
|
|
@@ -96,7 +105,7 @@ const _highlightText = (content, regexMatch, colorFunction, removeChars = null)
|
|
|
96
105
|
|
|
97
106
|
if (removeChars === null) {
|
|
98
107
|
|
|
99
|
-
let findAllRegex = new RegExp(match[0], "ig");
|
|
108
|
+
let findAllRegex = new RegExp(escapeRegex(match[0]), "ig");
|
|
100
109
|
|
|
101
110
|
newContent = newContent.replace(findAllRegex, colorFunction(match[1]));
|
|
102
111
|
|
|
@@ -221,6 +230,9 @@ const _codeBlock = (content) => {
|
|
|
221
230
|
* Formats source code blocks using PrismJS
|
|
222
231
|
* @param {string} content of MarkDown file
|
|
223
232
|
*/
|
|
233
|
+
// Module-level storage for code block placeholders
|
|
234
|
+
let _codeBlockPlaceholders = [];
|
|
235
|
+
|
|
224
236
|
const _highlightedCodeBlock = (content) => {
|
|
225
237
|
|
|
226
238
|
let codeRegex = new RegExp(/(\`\`\`)(.*?)(\`\`\`)/igs);
|
|
@@ -230,9 +242,13 @@ const _highlightedCodeBlock = (content) => {
|
|
|
230
242
|
return content;
|
|
231
243
|
}
|
|
232
244
|
|
|
245
|
+
// Reset and store highlighted code blocks with placeholders to protect from inline code processing
|
|
246
|
+
_codeBlockPlaceholders = [];
|
|
247
|
+
let placeholderIndex = 0;
|
|
248
|
+
|
|
233
249
|
newContent.forEach((element) => {
|
|
234
250
|
|
|
235
|
-
let langRegex = new RegExp(/(\`\`\`)(.*?)(\n)/igs);
|
|
251
|
+
let langRegex = new RegExp(/(\`\`\`)(.*?)(\r?\n)/igs);
|
|
236
252
|
let langIdentifiere = element.match(langRegex);
|
|
237
253
|
|
|
238
254
|
if (langIdentifiere.length === 1) {
|
|
@@ -250,7 +266,12 @@ const _highlightedCodeBlock = (content) => {
|
|
|
250
266
|
let hlSource = syntaxHighlighter.highlight(source, lang, this._theme.sourceCodeTheme);
|
|
251
267
|
this._theme.sourceCodeTheme;
|
|
252
268
|
|
|
253
|
-
|
|
269
|
+
// Use a placeholder that won't be matched by inline code regex
|
|
270
|
+
const placeholder = `__CODEBLOCK_${placeholderIndex}__`;
|
|
271
|
+
_codeBlockPlaceholders.push({ placeholder, code: hlSource });
|
|
272
|
+
placeholderIndex++;
|
|
273
|
+
|
|
274
|
+
content = content.replace(element, placeholder)
|
|
254
275
|
|
|
255
276
|
} catch (e) {
|
|
256
277
|
|
|
@@ -266,6 +287,14 @@ const _highlightedCodeBlock = (content) => {
|
|
|
266
287
|
|
|
267
288
|
}
|
|
268
289
|
|
|
290
|
+
const _restoreCodeBlocks = (content) => {
|
|
291
|
+
// Restore code blocks from placeholders
|
|
292
|
+
_codeBlockPlaceholders.forEach(({ placeholder, code }) => {
|
|
293
|
+
content = content.replace(placeholder, code);
|
|
294
|
+
});
|
|
295
|
+
return content;
|
|
296
|
+
}
|
|
297
|
+
|
|
269
298
|
/**
|
|
270
299
|
* Formats Blockquote
|
|
271
300
|
* @param {string} content of markdown file
|
|
@@ -379,7 +408,7 @@ const _includeExternals = (content, externalFound, baseDir, regexDelimiter) => {
|
|
|
379
408
|
|
|
380
409
|
for (let i = 0; i < keys.length; i++) {
|
|
381
410
|
|
|
382
|
-
var regExp = new RegExp(keys[i], 'ig');
|
|
411
|
+
var regExp = new RegExp(escapeRegex(keys[i]), 'ig');
|
|
383
412
|
newContent = newContent.replace(regExp, replacements[keys[i]]);
|
|
384
413
|
|
|
385
414
|
}
|
|
@@ -448,13 +477,23 @@ const _addBold = (content) => {
|
|
|
448
477
|
|
|
449
478
|
/**
|
|
450
479
|
* Formats italic elements in MarkDown
|
|
480
|
+
* Supports both underscore (_text_) and asterisk (*text*) emphasis markers
|
|
451
481
|
* @param {string} content of markdown file
|
|
452
482
|
*/
|
|
453
483
|
const _addItalic = (content) => {
|
|
454
484
|
|
|
455
|
-
|
|
485
|
+
// First pass: Handle underscore emphasis
|
|
486
|
+
// Pattern: _text_ where underscores are not adjacent to word characters
|
|
487
|
+
let underscoreRegex = new RegExp(/(?<!\w)_([^_\n]+?)_(?!\w)/g);
|
|
488
|
+
content = _highlightText(content, underscoreRegex, this._theme.italic);
|
|
456
489
|
|
|
457
|
-
|
|
490
|
+
// Second pass: Handle asterisk emphasis (but not bold **)
|
|
491
|
+
// Pattern: *text* where asterisks are not adjacent to other asterisks (to avoid matching **bold**)
|
|
492
|
+
// Also not adjacent to word characters on the outside to avoid matching mid-word asterisks
|
|
493
|
+
let asteriskRegex = new RegExp(/(?<!\*)(?<!\w)\*([^*\n]+?)\*(?!\*)(?!\w)/g);
|
|
494
|
+
content = _highlightText(content, asteriskRegex, this._theme.italic);
|
|
495
|
+
|
|
496
|
+
return content;
|
|
458
497
|
|
|
459
498
|
}
|
|
460
499
|
|
|
@@ -484,8 +523,10 @@ const _addCode = (content) => {
|
|
|
484
523
|
*/
|
|
485
524
|
const _addInlineCode = (content) => {
|
|
486
525
|
|
|
487
|
-
//
|
|
488
|
-
|
|
526
|
+
// Use negative lookbehind/lookahead to avoid matching backticks that are part of triple-backtick code blocks
|
|
527
|
+
// (?<!`) = not preceded by backtick, (?!`) = not followed by backtick
|
|
528
|
+
// This ensures we only match single backticks for inline code, not triple backticks for code blocks
|
|
529
|
+
return _highlightText(content, /(?<!`)`([^`]+)`(?!`)/g, this._theme.inlineCode);
|
|
489
530
|
|
|
490
531
|
}
|
|
491
532
|
|
|
@@ -651,7 +692,7 @@ const _toRawContent = (filepath) => {
|
|
|
651
692
|
}
|
|
652
693
|
|
|
653
694
|
try {
|
|
654
|
-
content = _removeImages(content,
|
|
695
|
+
content = _removeImages(content, /!\[([^\]]*)\]\(([^)"'\s]+)(?:\s+["']([^"']*)["'])?\)/g);
|
|
655
696
|
} catch (e) {
|
|
656
697
|
throw `Error in remove images: ${e}`;
|
|
657
698
|
}
|
|
@@ -673,17 +714,26 @@ const _toRawContent = (filepath) => {
|
|
|
673
714
|
|
|
674
715
|
|
|
675
716
|
try {
|
|
676
|
-
//
|
|
717
|
+
// Process code blocks FIRST - replaces them with placeholders
|
|
718
|
+
// This prevents inline code regex from matching backticks inside code blocks
|
|
719
|
+
content = _highlightedCodeBlock(content);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
throw `Error in Highlighted Code: ${e}`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
// Process inline code AFTER code blocks are replaced with placeholders
|
|
726
|
+
// At this point, code blocks are hidden as __CODEBLOCK_N__ so inline code won't match them
|
|
677
727
|
content = _addInlineCode(content);
|
|
678
728
|
} catch (e) {
|
|
679
729
|
throw `Error in addInlineCode: ${e}`;
|
|
680
730
|
}
|
|
681
731
|
|
|
682
732
|
try {
|
|
683
|
-
//
|
|
684
|
-
content =
|
|
733
|
+
// Restore code blocks from placeholders
|
|
734
|
+
content = _restoreCodeBlocks(content);
|
|
685
735
|
} catch (e) {
|
|
686
|
-
throw `Error in
|
|
736
|
+
throw `Error in restoring code blocks: ${e}`;
|
|
687
737
|
}
|
|
688
738
|
|
|
689
739
|
try {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export var theme: any;
|
|
2
2
|
export var themeTokenKeys: string[];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Information about loaded languages
|
|
6
|
+
*/
|
|
7
|
+
export interface LoadedLanguagesInfo {
|
|
8
|
+
count: number;
|
|
9
|
+
languages: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
*
|
|
5
14
|
* @param {string} source
|
|
@@ -7,6 +16,20 @@ export var themeTokenKeys: string[];
|
|
|
7
16
|
* @param {sourceTheme} outTheme
|
|
8
17
|
*/
|
|
9
18
|
declare function _highlight(source: string, language: string, outTheme: sourceTheme): string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves language aliases to canonical names
|
|
22
|
+
* @param language - Language identifier (may be an alias)
|
|
23
|
+
* @returns Canonical language name or null
|
|
24
|
+
*/
|
|
25
|
+
declare function _resolveLanguageAlias(language: string): string | null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get information about currently loaded languages
|
|
29
|
+
* @returns Statistics about loaded languages
|
|
30
|
+
*/
|
|
31
|
+
declare function _getLoadedLanguagesInfo(): LoadedLanguagesInfo;
|
|
32
|
+
|
|
10
33
|
type sourceTheme = string;
|
|
11
34
|
declare namespace sourceTheme {
|
|
12
35
|
const COY: string;
|
|
@@ -18,4 +41,11 @@ declare namespace sourceTheme {
|
|
|
18
41
|
const TOMORROW: string;
|
|
19
42
|
const TWILIGHT: string;
|
|
20
43
|
}
|
|
21
|
-
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
_highlight as highlight,
|
|
47
|
+
sourceTheme as themes,
|
|
48
|
+
sourceTheme as availableThemes,
|
|
49
|
+
_getLoadedLanguagesInfo as getLoadedLanguagesInfo,
|
|
50
|
+
_resolveLanguageAlias as resolveLanguageAlias
|
|
51
|
+
};
|
|
@@ -17,7 +17,75 @@ const prismjs = require('prismjs/prism');
|
|
|
17
17
|
// load all supported languages
|
|
18
18
|
// @ts-ignore
|
|
19
19
|
const loadLanguages = require('prismjs/components/');
|
|
20
|
-
|
|
20
|
+
// Languages will be loaded on-demand for better performance
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cache of loaded languages to avoid redundant loading
|
|
24
|
+
* @type {Set<string>}
|
|
25
|
+
*/
|
|
26
|
+
const loadedLanguagesCache = new Set();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Comprehensive language alias mapping based on PrismJS components
|
|
30
|
+
* Maps common aliases to their canonical language names
|
|
31
|
+
* @type {Object<string, string>}
|
|
32
|
+
*/
|
|
33
|
+
const languageAliases = {
|
|
34
|
+
// JavaScript ecosystem
|
|
35
|
+
'js': 'javascript',
|
|
36
|
+
'mjs': 'javascript',
|
|
37
|
+
'cjs': 'javascript',
|
|
38
|
+
'jsx': 'javascript',
|
|
39
|
+
'ts': 'typescript',
|
|
40
|
+
'tsx': 'typescript',
|
|
41
|
+
|
|
42
|
+
// Shell/Bash variants
|
|
43
|
+
'sh': 'bash',
|
|
44
|
+
'shell': 'bash',
|
|
45
|
+
'console': 'bash',
|
|
46
|
+
'command': 'bash',
|
|
47
|
+
'bash session': 'bash',
|
|
48
|
+
'zsh': 'bash',
|
|
49
|
+
|
|
50
|
+
// Python
|
|
51
|
+
'py': 'python',
|
|
52
|
+
'py3': 'python',
|
|
53
|
+
'python3': 'python',
|
|
54
|
+
|
|
55
|
+
// Ruby
|
|
56
|
+
'rb': 'ruby',
|
|
57
|
+
|
|
58
|
+
// Markup languages
|
|
59
|
+
'html': 'markup',
|
|
60
|
+
'xml': 'markup',
|
|
61
|
+
'svg': 'markup',
|
|
62
|
+
'mathml': 'markup',
|
|
63
|
+
'ssml': 'markup',
|
|
64
|
+
|
|
65
|
+
// C family
|
|
66
|
+
'c++': 'cpp',
|
|
67
|
+
'c#': 'csharp',
|
|
68
|
+
'cs': 'csharp',
|
|
69
|
+
|
|
70
|
+
// Other common aliases
|
|
71
|
+
'yml': 'yaml',
|
|
72
|
+
'md': 'markdown',
|
|
73
|
+
'dockerfile': 'docker',
|
|
74
|
+
'objc': 'objectivec',
|
|
75
|
+
'hs': 'haskell',
|
|
76
|
+
'rs': 'rust',
|
|
77
|
+
'kt': 'kotlin',
|
|
78
|
+
'proto': 'protobuf',
|
|
79
|
+
'coffee': 'coffeescript',
|
|
80
|
+
'gawk': 'awk',
|
|
81
|
+
'hbs': 'handlebars',
|
|
82
|
+
'mustache': 'handlebars',
|
|
83
|
+
'g4': 'antlr4',
|
|
84
|
+
'ino': 'arduino',
|
|
85
|
+
'adoc': 'asciidoc',
|
|
86
|
+
'idr': 'idris',
|
|
87
|
+
'eta': 'ejs',
|
|
88
|
+
};
|
|
21
89
|
|
|
22
90
|
/**
|
|
23
91
|
* @readonly
|
|
@@ -50,9 +118,9 @@ const getHighlightToken = (tokens) => {
|
|
|
50
118
|
|
|
51
119
|
for (let i = 0; i < tokens.length; i++) {
|
|
52
120
|
|
|
53
|
-
if (
|
|
121
|
+
if (themeTokenKeys.indexOf(tokens[i]) !== -1) {
|
|
54
122
|
|
|
55
|
-
tokenFound =
|
|
123
|
+
tokenFound = theme.token[tokens[i]];
|
|
56
124
|
break;
|
|
57
125
|
}
|
|
58
126
|
|
|
@@ -132,7 +200,7 @@ const _addBackground = (source, originalSource) => {
|
|
|
132
200
|
}
|
|
133
201
|
|
|
134
202
|
bgAddedSource.push(
|
|
135
|
-
|
|
203
|
+
theme.background(sourceLines[i]) + theme.toEOL((" ").repeat(fill2end))
|
|
136
204
|
);
|
|
137
205
|
|
|
138
206
|
}
|
|
@@ -142,14 +210,88 @@ const _addBackground = (source, originalSource) => {
|
|
|
142
210
|
}
|
|
143
211
|
|
|
144
212
|
/**
|
|
145
|
-
*
|
|
146
|
-
* @param {string}
|
|
147
|
-
* @
|
|
148
|
-
|
|
213
|
+
* Resolves language aliases to canonical names
|
|
214
|
+
* @param {string} language - Language identifier (may be an alias)
|
|
215
|
+
* @returns {string|null} - Canonical language name or null if no language provided
|
|
216
|
+
*/
|
|
217
|
+
const resolveLanguageAlias = (language) => {
|
|
218
|
+
if (!language) return null;
|
|
219
|
+
|
|
220
|
+
const normalized = language.toLowerCase().trim();
|
|
221
|
+
return languageAliases[normalized] || normalized;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Dynamically loads a language component if not already loaded
|
|
226
|
+
* @param {string} language - Language to load (canonical name or alias)
|
|
227
|
+
* @returns {boolean} - True if language is available, false otherwise
|
|
228
|
+
*/
|
|
229
|
+
const ensureLanguageLoaded = (language) => {
|
|
230
|
+
if (!language) return false;
|
|
231
|
+
|
|
232
|
+
// Resolve alias to canonical name
|
|
233
|
+
const canonicalLang = resolveLanguageAlias(language);
|
|
234
|
+
if (!canonicalLang) return false;
|
|
235
|
+
|
|
236
|
+
// Check if already loaded in our cache
|
|
237
|
+
if (loadedLanguagesCache.has(canonicalLang)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if language exists in Prism.languages (might be pre-loaded)
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
if (Prism.languages[canonicalLang] !== undefined) {
|
|
244
|
+
loadedLanguagesCache.add(canonicalLang);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Attempt to load the language
|
|
249
|
+
try {
|
|
250
|
+
// Suppress PrismJS warnings for this operation
|
|
251
|
+
const originalSilent = loadLanguages.silent;
|
|
252
|
+
loadLanguages.silent = true;
|
|
253
|
+
|
|
254
|
+
// Load the language (PrismJS handles dependencies automatically)
|
|
255
|
+
loadLanguages([canonicalLang]);
|
|
256
|
+
|
|
257
|
+
// Restore original silent setting
|
|
258
|
+
loadLanguages.silent = originalSilent;
|
|
259
|
+
|
|
260
|
+
// Verify it loaded successfully
|
|
261
|
+
// @ts-ignore
|
|
262
|
+
if (Prism.languages[canonicalLang] !== undefined) {
|
|
263
|
+
loadedLanguagesCache.add(canonicalLang);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// Language loading failed
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get information about loaded languages (for debugging/monitoring)
|
|
277
|
+
* @returns {{count: number, languages: string[]}} - Statistics about loaded languages
|
|
278
|
+
*/
|
|
279
|
+
const getLoadedLanguagesInfo = () => {
|
|
280
|
+
return {
|
|
281
|
+
count: loadedLanguagesCache.size,
|
|
282
|
+
languages: Array.from(loadedLanguagesCache).sort()
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
*
|
|
288
|
+
* @param {string} source
|
|
289
|
+
* @param {string} language
|
|
290
|
+
* @param {sourceTheme} outTheme
|
|
149
291
|
*/
|
|
150
292
|
const _highlight = (source, language, outTheme) => {
|
|
151
293
|
|
|
152
|
-
// Detect if theme value is supported - otherwise just use default
|
|
294
|
+
// Detect if theme value is supported - otherwise just use default Okaidia theme
|
|
153
295
|
if (outTheme !== undefined) {
|
|
154
296
|
|
|
155
297
|
let themePath = path.join(__dirname, './themes/', outTheme + '.theme'),
|
|
@@ -157,48 +299,51 @@ const _highlight = (source, language, outTheme) => {
|
|
|
157
299
|
|
|
158
300
|
if (fs.existsSync(filePath)) {
|
|
159
301
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
302
|
+
theme = require(filePath);
|
|
303
|
+
themeTokenKeys = Object.keys(theme.token);
|
|
163
304
|
|
|
164
305
|
} else {
|
|
165
306
|
|
|
166
|
-
throw `Theme '${outTheme}' do not
|
|
307
|
+
throw `Theme '${outTheme}' do not exist`
|
|
167
308
|
|
|
168
309
|
}
|
|
169
310
|
|
|
170
311
|
}
|
|
171
312
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// @ts-ignore
|
|
177
|
-
Prism.languages['console'] = Prism.languages['shell'];
|
|
178
|
-
// @ts-ignore
|
|
179
|
-
Prism.languages['command'] = Prism.languages['shell'];
|
|
180
|
-
// @ts-ignore
|
|
181
|
-
Prism.languages['bash session'] = Prism.languages['shell'];
|
|
313
|
+
// If no language specified, render as plain text
|
|
314
|
+
if (!language) {
|
|
315
|
+
return _addBackground(source, source);
|
|
316
|
+
}
|
|
182
317
|
|
|
183
|
-
//
|
|
184
|
-
|
|
318
|
+
// Dynamically load the language if needed
|
|
319
|
+
const languageLoaded = ensureLanguageLoaded(language);
|
|
320
|
+
|
|
321
|
+
if (!languageLoaded) {
|
|
322
|
+
// Language doesn't exist or failed to load - fallback to plain text
|
|
323
|
+
// Optionally log a warning (can be configured via environment variable)
|
|
324
|
+
if (process.env.MARKSHELL_WARN_UNKNOWN_LANG === 'true') {
|
|
325
|
+
console.warn(`[markshell] Unknown language '${language}' - rendering as plain text`);
|
|
326
|
+
}
|
|
327
|
+
return _addBackground(source, source);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Get the canonical language name for highlighting
|
|
331
|
+
const canonicalLang = resolveLanguageAlias(language);
|
|
332
|
+
|
|
333
|
+
// @ts-ignore - Prism is loaded globally
|
|
334
|
+
if (Prism.languages[canonicalLang] !== undefined) {
|
|
185
335
|
// @ts-ignore
|
|
186
|
-
const prismCode = prismjs.highlight(source, Prism.languages[
|
|
336
|
+
const prismCode = prismjs.highlight(source, Prism.languages[canonicalLang], canonicalLang);
|
|
187
337
|
// load HTML fragment
|
|
188
338
|
const dom = JSDOM.fragment(prismCode);
|
|
189
339
|
|
|
190
|
-
// console.log('Original Source:\n', source);
|
|
191
|
-
|
|
192
340
|
var highlightedSource = parseFormatedContent(dom.childNodes, 0);
|
|
193
341
|
|
|
194
342
|
return _addBackground(highlightedSource, source);
|
|
195
343
|
|
|
196
344
|
} else {
|
|
197
|
-
|
|
345
|
+
// Shouldn't reach here if ensureLanguageLoaded worked correctly
|
|
198
346
|
return _addBackground(source, source);
|
|
199
|
-
|
|
200
|
-
// throw `Language '${language}' couldn't be found`;
|
|
201
|
-
|
|
202
347
|
}
|
|
203
348
|
|
|
204
349
|
}
|
|
@@ -206,5 +351,7 @@ const _highlight = (source, language, outTheme) => {
|
|
|
206
351
|
module.exports = {
|
|
207
352
|
highlight: _highlight,
|
|
208
353
|
themes: sourceTheme,
|
|
209
|
-
availableThemes: sourceTheme
|
|
354
|
+
availableThemes: sourceTheme,
|
|
355
|
+
getLoadedLanguagesInfo: getLoadedLanguagesInfo,
|
|
356
|
+
resolveLanguageAlias: resolveLanguageAlias
|
|
210
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markshell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "markshell allows you to output any markdown file formatted and style to the console",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://github.com/stfbauer/markshell#readme",
|
|
24
24
|
"scripts": {
|
|
25
|
-
"test": "
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"test:ui": "vitest --ui",
|
|
27
|
+
"test:run": "vitest run",
|
|
28
|
+
"test:coverage": "vitest --coverage",
|
|
29
|
+
"test:old": "node test/index.js",
|
|
26
30
|
"testRun": "node ./lib/index.js",
|
|
27
31
|
"update-types": "./tools/typings.sh"
|
|
28
32
|
},
|
|
@@ -38,15 +42,18 @@
|
|
|
38
42
|
"author": "Stefan Bauer",
|
|
39
43
|
"license": "MIT",
|
|
40
44
|
"dependencies": {
|
|
41
|
-
"@types/prismjs": "1.
|
|
45
|
+
"@types/prismjs": "1.26",
|
|
42
46
|
"chalk": "^4.1.2",
|
|
43
|
-
"jsdom": "^
|
|
44
|
-
"prismjs": "^1.
|
|
45
|
-
"wrap-ansi": "^
|
|
47
|
+
"jsdom": "^27.3.0",
|
|
48
|
+
"prismjs": "^1.30.0",
|
|
49
|
+
"wrap-ansi": "^9.0.2"
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^10.
|
|
49
|
-
"@types/prismjs": "^1.16.1"
|
|
52
|
+
"@types/node": "^24.10.2",
|
|
53
|
+
"@types/prismjs": "^1.16.1",
|
|
54
|
+
"@vitest/coverage-v8": "^1.6.1",
|
|
55
|
+
"@vitest/ui": "^1.0.0",
|
|
56
|
+
"vitest": "^1.0.0"
|
|
50
57
|
},
|
|
51
58
|
"repository": {
|
|
52
59
|
"type": "git",
|