@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.
@@ -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;AAE3B,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;AAElC,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;IA4DJ,OAAO,CAAC,YAAY;IA8DpB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM;CAKrE"}
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"}
@@ -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.52",
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
+ });
@@ -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
  });