@hcengineering/text-markdown 0.7.1
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/lib/__tests__/markdown.test.js +1044 -0
- package/lib/__tests__/markdown.test.js.map +7 -0
- package/lib/compare.js +100 -0
- package/lib/compare.js.map +7 -0
- package/lib/index.js +47 -0
- package/lib/index.js.map +7 -0
- package/lib/marks.js +59 -0
- package/lib/marks.js.map +7 -0
- package/lib/node.js +34 -0
- package/lib/node.js.map +7 -0
- package/lib/parser.js +724 -0
- package/lib/parser.js.map +7 -0
- package/lib/serializer.js +614 -0
- package/lib/serializer.js.map +7 -0
- package/package.json +59 -0
- package/src/__tests__/markdown.test.ts +1076 -0
- package/src/compare.ts +119 -0
- package/src/index.ts +47 -0
- package/src/marks.ts +46 -0
- package/src/node.ts +24 -0
- package/src/parser.ts +853 -0
- package/src/serializer.ts +833 -0
- package/tsconfig.json +12 -0
- package/types/__tests__/markdown.test.d.ts +9 -0
- package/types/__tests__/markdown.test.d.ts.map +1 -0
- package/types/compare.d.ts +10 -0
- package/types/compare.d.ts.map +1 -0
- package/types/index.d.ts +14 -0
- package/types/index.d.ts.map +1 -0
- package/types/marks.d.ts +8 -0
- package/types/marks.d.ts.map +1 -0
- package/types/node.d.ts +4 -0
- package/types/node.d.ts.map +1 -0
- package/types/parser.d.ts +50 -0
- package/types/parser.d.ts.map +1 -0
- package/types/serializer.d.ts +102 -0
- package/types/serializer.d.ts.map +1 -0
package/src/compare.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Hardcore Engineering Inc.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License. You may
|
|
6
|
+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
//
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculate Sørensen–Dice coefficient
|
|
18
|
+
*/
|
|
19
|
+
export function calcSørensenDiceCoefficient (a: string, b: string): number {
|
|
20
|
+
const first = a.replace(/\s+/g, '')
|
|
21
|
+
const second = b.replace(/\s+/g, '')
|
|
22
|
+
|
|
23
|
+
if (first === second) return 1 // identical or empty
|
|
24
|
+
if (first.length < 2 || second.length < 2) return 0 // if either is a 0-letter or 1-letter string
|
|
25
|
+
|
|
26
|
+
const firstBigrams = new Map<string, number>()
|
|
27
|
+
for (let i = 0; i < first.length - 1; i++) {
|
|
28
|
+
const bigram = first.substring(i, i + 2)
|
|
29
|
+
const count = (firstBigrams.get(bigram) ?? 0) + 1
|
|
30
|
+
|
|
31
|
+
firstBigrams.set(bigram, count)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let intersectionSize = 0
|
|
35
|
+
for (let i = 0; i < second.length - 1; i++) {
|
|
36
|
+
const bigram = second.substring(i, i + 2)
|
|
37
|
+
const count = firstBigrams.get(bigram) ?? 0
|
|
38
|
+
|
|
39
|
+
if (count > 0) {
|
|
40
|
+
firstBigrams.set(bigram, count - 1)
|
|
41
|
+
intersectionSize++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (2.0 * intersectionSize) / (first.length + second.length - 2)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Perform markdown diff/comparison to understand do we have a major differences.
|
|
50
|
+
*/
|
|
51
|
+
export function isMarkdownsEquals (source1: string, source2: string): boolean {
|
|
52
|
+
const normalized1 = normalizeMarkdown(source1)
|
|
53
|
+
const normalized2 = normalizeMarkdown(source2)
|
|
54
|
+
return normalized1 === normalized2
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeMarkdown (source: string): string {
|
|
58
|
+
const tagRegex = /<(\w+)([^>]*?)(\/?)>/g
|
|
59
|
+
const attrRegex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g
|
|
60
|
+
|
|
61
|
+
// Normalize line endings to LF
|
|
62
|
+
source = source.replace(/\r?\n/g, '\n')
|
|
63
|
+
|
|
64
|
+
// Remove extra blank lines
|
|
65
|
+
source = source
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map((it) => it.trimEnd())
|
|
68
|
+
.filter((it) => it.length > 0)
|
|
69
|
+
.join('\n')
|
|
70
|
+
|
|
71
|
+
// Normalize HTML tags
|
|
72
|
+
source = source.replace(tagRegex, (match, tagName, attributes) => {
|
|
73
|
+
const attrs: Record<string, string> = {}
|
|
74
|
+
|
|
75
|
+
let attrMatch = attrRegex.exec(attributes)
|
|
76
|
+
while (attrMatch !== null) {
|
|
77
|
+
const attrName = attrMatch[1]
|
|
78
|
+
const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? ''
|
|
79
|
+
attrs[attrName] = attrValue
|
|
80
|
+
attrMatch = attrRegex.exec(attributes)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort attributes by name for consistent order
|
|
84
|
+
const sortedAttrs = Object.keys(attrs)
|
|
85
|
+
.sort()
|
|
86
|
+
.map((key) => {
|
|
87
|
+
const value = attrs[key]
|
|
88
|
+
return value !== '' ? `${key}="${value}"` : key
|
|
89
|
+
})
|
|
90
|
+
.join(' ')
|
|
91
|
+
|
|
92
|
+
// Normalize to self-closing format for void elements
|
|
93
|
+
const voidElements = [
|
|
94
|
+
'img',
|
|
95
|
+
'br',
|
|
96
|
+
'hr',
|
|
97
|
+
'input',
|
|
98
|
+
'meta',
|
|
99
|
+
'area',
|
|
100
|
+
'base',
|
|
101
|
+
'col',
|
|
102
|
+
'embed',
|
|
103
|
+
'link',
|
|
104
|
+
'param',
|
|
105
|
+
'source',
|
|
106
|
+
'track',
|
|
107
|
+
'wbr'
|
|
108
|
+
]
|
|
109
|
+
const isVoidElement = voidElements.includes(tagName.toLowerCase())
|
|
110
|
+
|
|
111
|
+
if (sortedAttrs !== '') {
|
|
112
|
+
return isVoidElement ? `<${tagName} ${sortedAttrs} />` : `<${tagName} ${sortedAttrs}>`
|
|
113
|
+
} else {
|
|
114
|
+
return isVoidElement ? `<${tagName} />` : `<${tagName}>`
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return source
|
|
119
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Hardcore Engineering Inc.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License. You may
|
|
6
|
+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
//
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
import { MarkupNode } from '@hcengineering/text-core'
|
|
17
|
+
import { MarkdownParser } from './parser'
|
|
18
|
+
import { MarkdownState, storeMarks, storeNodes } from './serializer'
|
|
19
|
+
|
|
20
|
+
export * from './compare'
|
|
21
|
+
export * from './parser'
|
|
22
|
+
export * from './serializer'
|
|
23
|
+
|
|
24
|
+
/** @public */
|
|
25
|
+
export interface MarkdownOptions {
|
|
26
|
+
refUrl?: string
|
|
27
|
+
imageUrl?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @public */
|
|
31
|
+
export function markupToMarkdown (markup: MarkupNode, options?: MarkdownOptions): string {
|
|
32
|
+
const refUrl = options?.refUrl ?? 'ref://'
|
|
33
|
+
const imageUrl = options?.imageUrl ?? 'image://'
|
|
34
|
+
|
|
35
|
+
const state = new MarkdownState(storeNodes, storeMarks, { tightLists: true, refUrl, imageUrl })
|
|
36
|
+
state.renderContent(markup)
|
|
37
|
+
return state.out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @public */
|
|
41
|
+
export function markdownToMarkup (markdown: string, options?: MarkdownOptions): MarkupNode {
|
|
42
|
+
const refUrl = options?.refUrl ?? 'ref://'
|
|
43
|
+
const imageUrl = options?.imageUrl ?? 'image://'
|
|
44
|
+
|
|
45
|
+
const parser = new MarkdownParser({ refUrl, imageUrl })
|
|
46
|
+
return parser.parse(markdown ?? '')
|
|
47
|
+
}
|
package/src/marks.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Hardcore Engineering Inc.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License. You may
|
|
6
|
+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
//
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
import { MarkupMark, MarkupMarkType } from '@hcengineering/text-core'
|
|
17
|
+
import { deepEqual } from 'fast-equals'
|
|
18
|
+
|
|
19
|
+
export function markAttrs (mark: MarkupMark): Record<string, string> {
|
|
20
|
+
return mark.attrs ?? {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isInSet (mark: MarkupMark, marks: MarkupMark[]): boolean {
|
|
24
|
+
return marks.find((m) => markEq(mark, m)) !== undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function addToSet (mark: MarkupMark, marks: MarkupMark[]): MarkupMark[] {
|
|
28
|
+
const m = marks.find((m) => markEq(mark, m))
|
|
29
|
+
if (m !== undefined) {
|
|
30
|
+
// We already have mark
|
|
31
|
+
return marks
|
|
32
|
+
}
|
|
33
|
+
return [...marks, mark]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function removeFromSet (markType: MarkupMarkType, marks: MarkupMark[]): MarkupMark[] {
|
|
37
|
+
return marks.filter((m) => m.type !== markType)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sameSet (a?: MarkupMark[], b?: MarkupMark[]): boolean {
|
|
41
|
+
return deepEqual(a, b)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function markEq (first: MarkupMark, other: MarkupMark): boolean {
|
|
45
|
+
return deepEqual(first, other)
|
|
46
|
+
}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Hardcore Engineering Inc.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License. You may
|
|
6
|
+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
//
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
import { Attrs, MarkupNode } from '@hcengineering/text-core'
|
|
17
|
+
|
|
18
|
+
export function nodeContent (node: MarkupNode): MarkupNode[] {
|
|
19
|
+
return node?.content ?? []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nodeAttrs (node: MarkupNode): Attrs {
|
|
23
|
+
return node.attrs ?? {}
|
|
24
|
+
}
|