@difizen/libro-markdown 0.3.52 → 0.3.54
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/es/markdown-render.d.ts +1 -0
- package/es/markdown-render.d.ts.map +1 -1
- package/es/markdown-render.js +18 -4
- package/package.json +3 -1
- package/src/markdown-render.spec.ts +102 -0
- package/src/markdown-render.ts +15 -2
package/es/markdown-render.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import MarkdownIt from 'markdown-it';
|
|
|
3
3
|
import type { MarkdownRenderOption } from './markdown-protocol.js';
|
|
4
4
|
import { MarkdownParser } from './markdown-protocol.js';
|
|
5
5
|
import 'katex/dist/katex.min.css';
|
|
6
|
+
import 'highlight.js/styles/github.css';
|
|
6
7
|
export declare class MarkdownRender implements MarkdownParser {
|
|
7
8
|
protected mkt: MarkdownIt;
|
|
8
9
|
slugify: (s: string) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-render.d.ts","sourceRoot":"","sources":["../src/markdown-render.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EAIrB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-render.d.ts","sourceRoot":"","sources":["../src/markdown-render.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EAIrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,0BAA0B,CAAC;AAClC,OAAO,gCAAgC,CAAC;AAExC,qBACa,cAAe,YAAW,cAAc;IACnD,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC;IAC1B,OAAO,wBAAW;IAClB,eAAe,UAAS;IACM,SAAS,CAAC,oBAAoB,EAAE,oBAAoB,CAAC;IAGnF,IAAI;IAsEJ,OAAO,CAAC,YAAY;IA+DpB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM;CAKrE"}
|
package/es/markdown-render.js
CHANGED
|
@@ -19,12 +19,14 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con
|
|
|
19
19
|
function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'transform-class-properties is enabled and runs after the decorators transform.'); }
|
|
20
20
|
import { ConfigurationService, inject, postConstruct, singleton } from '@difizen/mana-app';
|
|
21
21
|
import latexPlugin from '@traptitech/markdown-it-katex';
|
|
22
|
+
import hljs from 'highlight.js';
|
|
22
23
|
import MarkdownIt from 'markdown-it';
|
|
23
24
|
import sanitizeHtml from 'sanitize-html';
|
|
24
25
|
import { libroAnchor, linkInsideHeader, slugify } from "./anchor.js";
|
|
25
26
|
import { LibroMarkdownConfiguration } from "./config.js";
|
|
26
27
|
import { MarkdownParser } from "./markdown-protocol.js";
|
|
27
28
|
import 'katex/dist/katex.min.css';
|
|
29
|
+
import 'highlight.js/styles/github.css';
|
|
28
30
|
export var MarkdownRender = (_dec = singleton({
|
|
29
31
|
token: MarkdownParser
|
|
30
32
|
}), _dec2 = inject(ConfigurationService), _dec3 = postConstruct(), _dec(_class = (_class2 = /*#__PURE__*/function () {
|
|
@@ -40,7 +42,19 @@ export var MarkdownRender = (_dec = singleton({
|
|
|
40
42
|
var _this = this;
|
|
41
43
|
this.mkt = new MarkdownIt({
|
|
42
44
|
html: true,
|
|
43
|
-
linkify: true
|
|
45
|
+
linkify: true,
|
|
46
|
+
highlight: function highlight(str, lang) {
|
|
47
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
48
|
+
try {
|
|
49
|
+
return hljs.highlight(str, {
|
|
50
|
+
language: lang
|
|
51
|
+
}).value;
|
|
52
|
+
} catch (__) {
|
|
53
|
+
//
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return ''; // use external default escaping
|
|
57
|
+
}
|
|
44
58
|
});
|
|
45
59
|
this.mkt.linkify.set({
|
|
46
60
|
fuzzyLink: false
|
|
@@ -90,16 +104,16 @@ export var MarkdownRender = (_dec = singleton({
|
|
|
90
104
|
}, {
|
|
91
105
|
key: "sanitizeHTML",
|
|
92
106
|
value: function sanitizeHTML(html) {
|
|
93
|
-
var allowedTags = sanitizeHtml.defaults.allowedTags.concat(['a', 'abbr', 'acronym', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'q', 's', 'small', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'tr', 'tt', 'u', 'ul', 'kbd', 'var']);
|
|
107
|
+
var allowedTags = sanitizeHtml.defaults.allowedTags.concat(['a', 'abbr', 'acronym', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'q', 's', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'tr', 'tt', 'u', 'ul', 'kbd', 'var']);
|
|
94
108
|
// 构建新的 allowedAttributes,为所有允许的标签添加 'id'
|
|
95
109
|
var allowedAttributes = Object.fromEntries(allowedTags.map(function (tag) {
|
|
96
|
-
return [tag, [].concat(_toConsumableArray(sanitizeHtml.defaults.allowedAttributes[tag] || []), ['id'])];
|
|
110
|
+
return [tag, [].concat(_toConsumableArray(sanitizeHtml.defaults.allowedAttributes[tag] || []), ['id', 'class'])];
|
|
97
111
|
}));
|
|
98
112
|
return sanitizeHtml(html, {
|
|
99
113
|
allowedTags: allowedTags,
|
|
100
114
|
// 允许的标签
|
|
101
115
|
allowedAttributes: _objectSpread(_objectSpread({}, allowedAttributes), {}, {
|
|
102
|
-
a: ['href', 'title', 'id'],
|
|
116
|
+
a: ['href', 'title', 'id', 'target'],
|
|
103
117
|
img: ['src', 'alt', 'id']
|
|
104
118
|
})
|
|
105
119
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@difizen/libro-markdown",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.54",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"libro",
|
|
@@ -37,12 +37,14 @@
|
|
|
37
37
|
"@difizen/mana-l10n": "latest",
|
|
38
38
|
"@traptitech/markdown-it-katex": "^3.6.0",
|
|
39
39
|
"@types/markdown-it": "^12.2.3",
|
|
40
|
+
"highlight.js": "^11.11.1",
|
|
40
41
|
"katex": "^0.16.10",
|
|
41
42
|
"markdown-it": "^13.0.1",
|
|
42
43
|
"markdown-it-anchor": "^8.6.5",
|
|
43
44
|
"sanitize-html": "^2.14.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
47
|
+
"@types/highlight.js": "^10.1.0",
|
|
46
48
|
"@types/sanitize-html": "^2.13.0"
|
|
47
49
|
},
|
|
48
50
|
"scripts": {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
import { MarkdownRender } from './markdown-render.js';
|
|
5
|
+
|
|
6
|
+
// Declare jest globals to avoid compilation errors if types are missing
|
|
7
|
+
declare const describe: any;
|
|
8
|
+
declare const it: any;
|
|
9
|
+
declare const beforeEach: any;
|
|
10
|
+
declare const jest: any;
|
|
11
|
+
|
|
12
|
+
// Mock ConfigurationService
|
|
13
|
+
const mockConfigService = {
|
|
14
|
+
get: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('MarkdownRender', () => {
|
|
18
|
+
let markdownRender: MarkdownRender;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Reset mock and set default behavior
|
|
22
|
+
mockConfigService.get.mockReset();
|
|
23
|
+
mockConfigService.get.mockResolvedValue(false);
|
|
24
|
+
|
|
25
|
+
markdownRender = new MarkdownRender();
|
|
26
|
+
// Inject mock manually
|
|
27
|
+
(markdownRender as any).configurationService = mockConfigService;
|
|
28
|
+
// Manually call init to trigger postConstruct logic
|
|
29
|
+
markdownRender.init();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should render basic markdown', () => {
|
|
33
|
+
const md = '# Hello';
|
|
34
|
+
const html = markdownRender.render(md);
|
|
35
|
+
// h1 id="hello" comes from anchor plugin
|
|
36
|
+
assert.ok(html.includes('<h1 id="hello">Hello</h1>'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should highlight code blocks', () => {
|
|
40
|
+
const md = '```javascript\nconst a = 1;\n```';
|
|
41
|
+
const html = markdownRender.render(md);
|
|
42
|
+
|
|
43
|
+
// Check for language class added by markdown-it/highlight.js
|
|
44
|
+
assert.ok(html.includes('language-javascript'));
|
|
45
|
+
|
|
46
|
+
// Check for highlight.js specific classes (indicating highlighting actually happened)
|
|
47
|
+
// "const" is a keyword
|
|
48
|
+
assert.ok(html.includes('hljs-keyword'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should sanitize html', () => {
|
|
52
|
+
const md = '<script>alert(1)</script>';
|
|
53
|
+
const html = markdownRender.render(md);
|
|
54
|
+
assert.ok(!html.includes('<script>'));
|
|
55
|
+
assert.ok(!html.includes('alert(1)'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should allow span tags with class attributes (needed for highlighting)', () => {
|
|
59
|
+
// Manually construct a highlighted-like span to ensure sanitizer doesn't strip it
|
|
60
|
+
// Note: markdown-it render output is sanitized.
|
|
61
|
+
// If we input raw HTML, it might be stripped depending on settings.
|
|
62
|
+
// But highlight.js output is generated internally.
|
|
63
|
+
// Let's test with a code block that generates spans.
|
|
64
|
+
|
|
65
|
+
const md = '```javascript\nconst a = 1;\n```';
|
|
66
|
+
const html = markdownRender.render(md);
|
|
67
|
+
|
|
68
|
+
// Check that span tags are preserved
|
|
69
|
+
assert.ok(html.includes('<span class="hljs-keyword">const</span>'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support target="_blank" configuration', async () => {
|
|
73
|
+
mockConfigService.get.mockResolvedValue(true);
|
|
74
|
+
|
|
75
|
+
const renderer = new MarkdownRender();
|
|
76
|
+
(renderer as any).configurationService = mockConfigService;
|
|
77
|
+
renderer.init();
|
|
78
|
+
|
|
79
|
+
// Wait for promise resolution in init (microtask)
|
|
80
|
+
// Increase wait time to ensure the promise chain in init() completes
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
82
|
+
|
|
83
|
+
const md = '[link](http://example.com)';
|
|
84
|
+
const html = renderer.render(md);
|
|
85
|
+
assert.ok(html.includes('target="_blank"'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should NOT add target="_blank" when disabled', async () => {
|
|
89
|
+
mockConfigService.get.mockResolvedValue(false);
|
|
90
|
+
|
|
91
|
+
const renderer = new MarkdownRender();
|
|
92
|
+
(renderer as any).configurationService = mockConfigService;
|
|
93
|
+
renderer.init();
|
|
94
|
+
|
|
95
|
+
// Wait for promise resolution in init
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
97
|
+
|
|
98
|
+
const md = '[link](http://example.com)';
|
|
99
|
+
const html = renderer.render(md);
|
|
100
|
+
assert.ok(!html.includes('target="_blank"'));
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/markdown-render.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
singleton,
|
|
6
6
|
} from '@difizen/mana-app';
|
|
7
7
|
import latexPlugin from '@traptitech/markdown-it-katex';
|
|
8
|
+
import hljs from 'highlight.js';
|
|
8
9
|
import MarkdownIt from 'markdown-it';
|
|
9
10
|
import sanitizeHtml from 'sanitize-html';
|
|
10
11
|
|
|
@@ -13,6 +14,7 @@ import { LibroMarkdownConfiguration } from './config.js';
|
|
|
13
14
|
import type { MarkdownRenderOption } from './markdown-protocol.js';
|
|
14
15
|
import { MarkdownParser } from './markdown-protocol.js';
|
|
15
16
|
import 'katex/dist/katex.min.css';
|
|
17
|
+
import 'highlight.js/styles/github.css';
|
|
16
18
|
|
|
17
19
|
@singleton({ token: MarkdownParser })
|
|
18
20
|
export class MarkdownRender implements MarkdownParser {
|
|
@@ -26,6 +28,16 @@ export class MarkdownRender implements MarkdownParser {
|
|
|
26
28
|
this.mkt = new MarkdownIt({
|
|
27
29
|
html: true,
|
|
28
30
|
linkify: true,
|
|
31
|
+
highlight: function (str, lang) {
|
|
32
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
33
|
+
try {
|
|
34
|
+
return hljs.highlight(str, { language: lang }).value;
|
|
35
|
+
} catch (__) {
|
|
36
|
+
//
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ''; // use external default escaping
|
|
40
|
+
},
|
|
29
41
|
});
|
|
30
42
|
this.mkt.linkify.set({ fuzzyLink: false });
|
|
31
43
|
this.mkt.use(libroAnchor, {
|
|
@@ -113,6 +125,7 @@ export class MarkdownRender implements MarkdownParser {
|
|
|
113
125
|
'q',
|
|
114
126
|
's',
|
|
115
127
|
'small',
|
|
128
|
+
'span',
|
|
116
129
|
'strong',
|
|
117
130
|
'sub',
|
|
118
131
|
'sup',
|
|
@@ -131,14 +144,14 @@ export class MarkdownRender implements MarkdownParser {
|
|
|
131
144
|
const allowedAttributes = Object.fromEntries(
|
|
132
145
|
allowedTags.map((tag) => [
|
|
133
146
|
tag,
|
|
134
|
-
[...(sanitizeHtml.defaults.allowedAttributes[tag] || []), 'id'],
|
|
147
|
+
[...(sanitizeHtml.defaults.allowedAttributes[tag] || []), 'id', 'class'],
|
|
135
148
|
]),
|
|
136
149
|
);
|
|
137
150
|
return sanitizeHtml(html, {
|
|
138
151
|
allowedTags, // 允许的标签
|
|
139
152
|
allowedAttributes: {
|
|
140
153
|
...allowedAttributes,
|
|
141
|
-
a: ['href', 'title', 'id'],
|
|
154
|
+
a: ['href', 'title', 'id', 'target'],
|
|
142
155
|
img: ['src', 'alt', 'id'],
|
|
143
156
|
},
|
|
144
157
|
});
|