@jhl548/duplicate-doc-core 0.1.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/README.md +31 -0
- package/dist/colors.d.ts +8 -0
- package/dist/colors.js +36 -0
- package/dist/colors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/ranges.d.ts +5 -0
- package/dist/ranges.js +39 -0
- package/dist/ranges.js.map +1 -0
- package/dist/scroll.d.ts +6 -0
- package/dist/scroll.js +59 -0
- package/dist/scroll.js.map +1 -0
- package/dist/tiptapDuplicateHighlight.d.ts +21 -0
- package/dist/tiptapDuplicateHighlight.js +101 -0
- package/dist/tiptapDuplicateHighlight.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
- package/src/style.css +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @jhl548/duplicate-doc-core
|
|
2
|
+
|
|
3
|
+
`@jhl548/duplicate-doc-core` provides framework-independent types and helpers for duplicate document highlighting.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Shared document, range, duplicate point, and editor snapshot types.
|
|
8
|
+
- Similarity-based highlight color and CSS class helpers.
|
|
9
|
+
- Range normalization and document filtering utilities.
|
|
10
|
+
- Tiptap duplicate highlight extension.
|
|
11
|
+
- Shared highlight styles via `@jhl548/duplicate-doc-core/style.css`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @jhl548/duplicate-doc-core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
DuplicateHighlightExtension,
|
|
24
|
+
filterHighlightsForDocument,
|
|
25
|
+
type DuplicateHighlight,
|
|
26
|
+
type NormalizedDocument
|
|
27
|
+
} from "@jhl548/duplicate-doc-core";
|
|
28
|
+
import "@jhl548/duplicate-doc-core/style.css";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This package is intended to be consumed directly by framework adapters such as `@jhl548/duplicate-doc-vue`, or by applications that need access to the shared duplicate document model.
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface HighlightColor {
|
|
2
|
+
level: "low" | "medium" | "high";
|
|
3
|
+
className: string;
|
|
4
|
+
backgroundColor: string;
|
|
5
|
+
borderColor: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getHighlightColor(similarity: number): HighlightColor;
|
|
8
|
+
export declare function createHighlightClass(similarity: number, active?: boolean, ignored?: boolean): string;
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function getHighlightColor(similarity) {
|
|
2
|
+
if (similarity >= 0.85) {
|
|
3
|
+
return {
|
|
4
|
+
level: "high",
|
|
5
|
+
className: "dupdoc-highlight--high",
|
|
6
|
+
backgroundColor: "rgba(239, 68, 68, 0.22)",
|
|
7
|
+
borderColor: "rgba(239, 68, 68, 0.75)"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (similarity >= 0.65) {
|
|
11
|
+
return {
|
|
12
|
+
level: "medium",
|
|
13
|
+
className: "dupdoc-highlight--medium",
|
|
14
|
+
backgroundColor: "rgba(245, 158, 11, 0.24)",
|
|
15
|
+
borderColor: "rgba(245, 158, 11, 0.8)"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
level: "low",
|
|
20
|
+
className: "dupdoc-highlight--low",
|
|
21
|
+
backgroundColor: "rgba(59, 130, 246, 0.2)",
|
|
22
|
+
borderColor: "rgba(59, 130, 246, 0.7)"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function createHighlightClass(similarity, active = false, ignored = false) {
|
|
26
|
+
const color = getHighlightColor(similarity);
|
|
27
|
+
return [
|
|
28
|
+
"dupdoc-highlight",
|
|
29
|
+
color.className,
|
|
30
|
+
active ? "dupdoc-highlight--active" : "",
|
|
31
|
+
ignored ? "dupdoc-highlight--ignored" : ""
|
|
32
|
+
]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(" ");
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=colors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colors.js","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;QACvB,OAAO;YACL,KAAK,EAAE,MAAM;YACb,SAAS,EAAE,wBAAwB;YACnC,eAAe,EAAE,yBAAyB;YAC1C,WAAW,EAAE,yBAAyB;SACvC,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;QACvB,OAAO;YACL,KAAK,EAAE,QAAQ;YACf,SAAS,EAAE,0BAA0B;YACrC,eAAe,EAAE,0BAA0B;YAC3C,WAAW,EAAE,yBAAyB;SACvC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,SAAS,EAAE,uBAAuB;QAClC,eAAe,EAAE,yBAAyB;QAC1C,WAAW,EAAE,yBAAyB;KACvC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,MAAM,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK;IACtF,MAAM,KAAK,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAC5C,OAAO;QACL,kBAAkB;QAClB,KAAK,CAAC,SAAS;QACf,MAAM,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE;QACxC,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,EAAE;KAC3C;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,SAAS,CAAC"}
|
package/dist/ranges.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DuplicateHighlight, TextRange } from "./types";
|
|
2
|
+
export declare function normalizeRange(range: TextRange): TextRange | null;
|
|
3
|
+
export declare function normalizeRanges(ranges: TextRange[]): TextRange[];
|
|
4
|
+
export declare function filterHighlightsForDocument(highlights: DuplicateHighlight[], documentId: string): DuplicateHighlight[];
|
|
5
|
+
export declare function plainTextFromHtml(html: string): string;
|
package/dist/ranges.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function normalizeRange(range) {
|
|
2
|
+
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const start = Math.max(0, Math.min(range.start, range.end));
|
|
6
|
+
const end = Math.max(start, Math.max(range.start, range.end));
|
|
7
|
+
if (start === end) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
...range,
|
|
12
|
+
start,
|
|
13
|
+
end
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function normalizeRanges(ranges) {
|
|
17
|
+
return ranges
|
|
18
|
+
.map(normalizeRange)
|
|
19
|
+
.filter((range) => Boolean(range))
|
|
20
|
+
.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
21
|
+
}
|
|
22
|
+
export function filterHighlightsForDocument(highlights, documentId) {
|
|
23
|
+
return highlights
|
|
24
|
+
.filter((highlight) => highlight.documentId === documentId)
|
|
25
|
+
.map((highlight) => ({
|
|
26
|
+
...highlight,
|
|
27
|
+
ranges: normalizeRanges(highlight.ranges)
|
|
28
|
+
}))
|
|
29
|
+
.filter((highlight) => highlight.ranges.length > 0);
|
|
30
|
+
}
|
|
31
|
+
export function plainTextFromHtml(html) {
|
|
32
|
+
if (typeof document === "undefined") {
|
|
33
|
+
return html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
|
|
34
|
+
}
|
|
35
|
+
const element = document.createElement("div");
|
|
36
|
+
element.innerHTML = html;
|
|
37
|
+
return (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=ranges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ranges.js","sourceRoot":"","sources":["../src/ranges.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9D,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,GAAG,KAAK;QACR,KAAK;QACL,GAAG;KACJ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAmB;IACjD,OAAO,MAAM;SACV,GAAG,CAAC,cAAc,CAAC;SACnB,MAAM,CAAC,CAAC,KAAK,EAAsB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;SACrD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,UAAgC,EAChC,UAAkB;IAElB,OAAO,UAAU;SACd,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,KAAK,UAAU,CAAC;SAC1D,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACnB,GAAG,SAAS;QACZ,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC;KAC1C,CAAC,CAAC;SACF,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACjE,CAAC"}
|
package/dist/scroll.d.ts
ADDED
package/dist/scroll.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function scrollToActiveHighlight(root, options = {}) {
|
|
2
|
+
if (!root) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const selector = options.selector ?? ".dupdoc-highlight--active, .dupdoc-highlight";
|
|
6
|
+
const target = root.querySelector(selector);
|
|
7
|
+
if (!target) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const scrollContainer = findNearestScrollContainer(root, target);
|
|
11
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
12
|
+
const targetRect = target.getBoundingClientRect();
|
|
13
|
+
const block = options.block ?? "center";
|
|
14
|
+
const topDelta = targetRect.top - containerRect.top;
|
|
15
|
+
const nextTop = getNextScrollTop(scrollContainer, topDelta, targetRect.height, block);
|
|
16
|
+
scrollContainer.scrollTo({
|
|
17
|
+
top: nextTop,
|
|
18
|
+
left: getNextScrollLeft(scrollContainer, targetRect, containerRect),
|
|
19
|
+
behavior: options.behavior ?? "smooth"
|
|
20
|
+
});
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function findNearestScrollContainer(root, target) {
|
|
24
|
+
let current = target.parentElement;
|
|
25
|
+
while (current && current !== root) {
|
|
26
|
+
if (isScrollable(current)) {
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
current = current.parentElement;
|
|
30
|
+
}
|
|
31
|
+
return isScrollable(root) ? root : root.querySelector(".dupdoc-editor__body") ?? root;
|
|
32
|
+
}
|
|
33
|
+
function isScrollable(element) {
|
|
34
|
+
const style = window.getComputedStyle(element);
|
|
35
|
+
const canScrollY = /(auto|scroll|overlay)/.test(style.overflowY);
|
|
36
|
+
return canScrollY && element.scrollHeight > element.clientHeight;
|
|
37
|
+
}
|
|
38
|
+
function getNextScrollTop(scrollContainer, topDelta, targetHeight, block) {
|
|
39
|
+
if (block === "start") {
|
|
40
|
+
return scrollContainer.scrollTop + topDelta;
|
|
41
|
+
}
|
|
42
|
+
if (block === "end") {
|
|
43
|
+
return scrollContainer.scrollTop + topDelta - scrollContainer.clientHeight + targetHeight;
|
|
44
|
+
}
|
|
45
|
+
if (block === "nearest" && topDelta >= 0 && topDelta + targetHeight <= scrollContainer.clientHeight) {
|
|
46
|
+
return scrollContainer.scrollTop;
|
|
47
|
+
}
|
|
48
|
+
return scrollContainer.scrollTop + topDelta - scrollContainer.clientHeight / 2 + targetHeight / 2;
|
|
49
|
+
}
|
|
50
|
+
function getNextScrollLeft(scrollContainer, targetRect, containerRect) {
|
|
51
|
+
if (targetRect.left < containerRect.left) {
|
|
52
|
+
return scrollContainer.scrollLeft + targetRect.left - containerRect.left;
|
|
53
|
+
}
|
|
54
|
+
if (targetRect.right > containerRect.right) {
|
|
55
|
+
return scrollContainer.scrollLeft + targetRect.right - containerRect.right;
|
|
56
|
+
}
|
|
57
|
+
return scrollContainer.scrollLeft;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=scroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll.js","sourceRoot":"","sources":["../src/scroll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
3
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
4
|
+
import { DecorationSet } from "@tiptap/pm/view";
|
|
5
|
+
import type { DuplicateHighlight, TextRange } from "./types";
|
|
6
|
+
declare module "@tiptap/core" {
|
|
7
|
+
interface Commands<ReturnType> {
|
|
8
|
+
duplicateHighlight: {
|
|
9
|
+
setDuplicateHighlights: (highlights: DuplicateHighlight[]) => ReturnType;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export declare const duplicateHighlightPluginKey: PluginKey<DecorationSet>;
|
|
14
|
+
export declare function buildPlainTextPositionMap(doc: ProseMirrorNode): number[];
|
|
15
|
+
export declare function mapTextRangeToDocPositions(positions: number[], range: TextRange): {
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
} | null;
|
|
19
|
+
export declare const DuplicateHighlightExtension: Extension<any, {
|
|
20
|
+
highlights: DuplicateHighlight[];
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
import { createHighlightClass } from "./colors";
|
|
5
|
+
import { normalizeRanges } from "./ranges";
|
|
6
|
+
export const duplicateHighlightPluginKey = new PluginKey("duplicate-highlight");
|
|
7
|
+
export function buildPlainTextPositionMap(doc) {
|
|
8
|
+
const positions = [];
|
|
9
|
+
doc.forEach((blockNode, blockOffset, blockIndex) => {
|
|
10
|
+
// 后端 plainText 以换行分隔块级节点,这里补齐同样的偏移,避免第二段以后定位偏移。
|
|
11
|
+
if (blockIndex > 0) {
|
|
12
|
+
positions.push(blockOffset);
|
|
13
|
+
}
|
|
14
|
+
blockNode.descendants((node, relativePos) => {
|
|
15
|
+
if (!node.isText) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const text = node.text ?? "";
|
|
19
|
+
const absolutePos = blockOffset + 1 + relativePos;
|
|
20
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
21
|
+
positions.push(absolutePos + index);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return positions;
|
|
27
|
+
}
|
|
28
|
+
export function mapTextRangeToDocPositions(positions, range) {
|
|
29
|
+
const from = positions[range.start];
|
|
30
|
+
const lastIncludedPosition = positions[Math.max(range.start, range.end - 1)];
|
|
31
|
+
if (from === undefined || lastIncludedPosition === undefined) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const to = lastIncludedPosition + 1;
|
|
35
|
+
return to > from ? { from, to } : null;
|
|
36
|
+
}
|
|
37
|
+
function buildDecorations(doc, highlights) {
|
|
38
|
+
const decorations = [];
|
|
39
|
+
const positions = buildPlainTextPositionMap(doc);
|
|
40
|
+
for (const highlight of highlights) {
|
|
41
|
+
const ranges = normalizeRanges(highlight.ranges);
|
|
42
|
+
for (const range of ranges) {
|
|
43
|
+
const mappedRange = mapTextRangeToDocPositions(positions, range);
|
|
44
|
+
if (!mappedRange) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
decorations.push(Decoration.inline(mappedRange.from, mappedRange.to, {
|
|
48
|
+
class: createHighlightClass(highlight.similarity, highlight.active, highlight.ignored),
|
|
49
|
+
"data-duplicate-id": highlight.duplicateId,
|
|
50
|
+
"data-section-path": highlight.sectionPath?.join(" / ") ?? range.sectionPath?.join(" / "),
|
|
51
|
+
"data-region": highlight.region ?? range.region,
|
|
52
|
+
"data-semantic-type": highlight.semanticType ?? range.semanticType,
|
|
53
|
+
"data-noise-reason": highlight.noiseReason,
|
|
54
|
+
"data-table-id": highlight.tableContext?.tableId ?? range.tableContext?.tableId
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return DecorationSet.create(doc, decorations);
|
|
59
|
+
}
|
|
60
|
+
export const DuplicateHighlightExtension = Extension.create({
|
|
61
|
+
name: "duplicateHighlight",
|
|
62
|
+
addStorage() {
|
|
63
|
+
return {
|
|
64
|
+
highlights: []
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
addCommands() {
|
|
68
|
+
return {
|
|
69
|
+
setDuplicateHighlights: (highlights) => ({ tr, dispatch }) => {
|
|
70
|
+
this.storage.highlights = highlights;
|
|
71
|
+
if (dispatch) {
|
|
72
|
+
dispatch(tr.setMeta(duplicateHighlightPluginKey, { highlights }));
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
addProseMirrorPlugins() {
|
|
79
|
+
return [
|
|
80
|
+
new Plugin({
|
|
81
|
+
key: duplicateHighlightPluginKey,
|
|
82
|
+
state: {
|
|
83
|
+
init: (_, state) => buildDecorations(state.doc, this.storage.highlights),
|
|
84
|
+
apply: (tr, decorationSet, _oldState, newState) => {
|
|
85
|
+
const meta = tr.getMeta(duplicateHighlightPluginKey);
|
|
86
|
+
if (meta?.highlights) {
|
|
87
|
+
return buildDecorations(newState.doc, meta.highlights);
|
|
88
|
+
}
|
|
89
|
+
return decorationSet.map(tr.mapping, tr.doc);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
props: {
|
|
93
|
+
decorations(state) {
|
|
94
|
+
return duplicateHighlightPluginKey.getState(state);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=tiptapDuplicateHighlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tiptapDuplicateHighlight.js","sourceRoot":"","sources":["../src/tiptapDuplicateHighlight.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type DocumentRole = "main" | "slave";
|
|
2
|
+
export type DocumentRegion = "body" | "header" | "footer" | "table" | "caption" | "unknown";
|
|
3
|
+
export type DuplicateSeverity = "noise" | "low" | "medium" | "high";
|
|
4
|
+
export type TenderSemanticType = "qualification" | "priceTable" | "deviationTable" | "personnel" | "performance" | "schedule" | "technical" | "business" | "projectInfo" | "unknown";
|
|
5
|
+
export interface TableCellContext {
|
|
6
|
+
tableId: string;
|
|
7
|
+
rowIndex: number;
|
|
8
|
+
cellIndex: number;
|
|
9
|
+
headerText?: string;
|
|
10
|
+
rowHeaderText?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TextRange {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
blockId?: string;
|
|
16
|
+
sectionPath?: string[];
|
|
17
|
+
region?: DocumentRegion;
|
|
18
|
+
tableContext?: TableCellContext;
|
|
19
|
+
confidence?: number;
|
|
20
|
+
semanticType?: TenderSemanticType;
|
|
21
|
+
}
|
|
22
|
+
export interface RangeMapEntry {
|
|
23
|
+
blockId: string;
|
|
24
|
+
textStart: number;
|
|
25
|
+
textEnd: number;
|
|
26
|
+
selector?: string;
|
|
27
|
+
sectionPath?: string[];
|
|
28
|
+
region?: DocumentRegion;
|
|
29
|
+
listMarker?: string;
|
|
30
|
+
tableContext?: TableCellContext;
|
|
31
|
+
semanticType?: TenderSemanticType;
|
|
32
|
+
}
|
|
33
|
+
export interface DocumentAsset {
|
|
34
|
+
id: string;
|
|
35
|
+
url: string;
|
|
36
|
+
type: "image" | "font" | "other";
|
|
37
|
+
}
|
|
38
|
+
export interface NormalizedDocument {
|
|
39
|
+
documentId: string;
|
|
40
|
+
role: DocumentRole;
|
|
41
|
+
fileName: string;
|
|
42
|
+
html: string;
|
|
43
|
+
plainText: string;
|
|
44
|
+
rangeMap: RangeMapEntry[];
|
|
45
|
+
assets?: DocumentAsset[];
|
|
46
|
+
meta?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface DuplicateHighlight {
|
|
49
|
+
duplicateId: string;
|
|
50
|
+
documentId: string;
|
|
51
|
+
similarity: number;
|
|
52
|
+
ranges: TextRange[];
|
|
53
|
+
active?: boolean;
|
|
54
|
+
label?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
ignored?: boolean;
|
|
57
|
+
severity?: DuplicateSeverity;
|
|
58
|
+
sectionPath?: string[];
|
|
59
|
+
region?: DocumentRegion;
|
|
60
|
+
semanticType?: TenderSemanticType;
|
|
61
|
+
noiseReason?: string;
|
|
62
|
+
tableContext?: TableCellContext;
|
|
63
|
+
}
|
|
64
|
+
export interface DuplicatePoint {
|
|
65
|
+
duplicateId: string;
|
|
66
|
+
groupId?: string;
|
|
67
|
+
similarity: number;
|
|
68
|
+
label?: string;
|
|
69
|
+
summary?: string;
|
|
70
|
+
reason?: string;
|
|
71
|
+
ignored?: boolean;
|
|
72
|
+
severity?: DuplicateSeverity;
|
|
73
|
+
semanticType?: TenderSemanticType;
|
|
74
|
+
region?: DocumentRegion;
|
|
75
|
+
noiseReason?: string;
|
|
76
|
+
main: {
|
|
77
|
+
documentId: string;
|
|
78
|
+
ranges: TextRange[];
|
|
79
|
+
};
|
|
80
|
+
slaves: Array<{
|
|
81
|
+
documentId: string;
|
|
82
|
+
ranges: TextRange[];
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
export interface EditorChangePayload {
|
|
86
|
+
documentId: string;
|
|
87
|
+
html: string;
|
|
88
|
+
plainText: string;
|
|
89
|
+
json?: unknown;
|
|
90
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhl548/duplicate-doc-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./style.css": "./src/style.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src/style.css",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@tiptap/core": "^3.23.4",
|
|
30
|
+
"@tiptap/pm": "^3.23.4"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@tiptap/core": "^3.23.4"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"**/*.css"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/src/style.css
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.dupdoc-highlight {
|
|
2
|
+
border-radius: 4px;
|
|
3
|
+
box-decoration-break: clone;
|
|
4
|
+
-webkit-box-decoration-break: clone;
|
|
5
|
+
padding: 0 2px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.dupdoc-highlight--high {
|
|
9
|
+
background: rgba(239, 68, 68, 0.22);
|
|
10
|
+
border-bottom: 2px solid rgba(239, 68, 68, 0.75);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dupdoc-highlight--medium {
|
|
14
|
+
background: rgba(245, 158, 11, 0.24);
|
|
15
|
+
border-bottom: 2px solid rgba(245, 158, 11, 0.8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.dupdoc-highlight--low {
|
|
19
|
+
background: rgba(59, 130, 246, 0.2);
|
|
20
|
+
border-bottom: 2px solid rgba(59, 130, 246, 0.7);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dupdoc-highlight--active {
|
|
24
|
+
outline: 2px solid rgba(15, 23, 42, 0.3);
|
|
25
|
+
outline-offset: 2px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.dupdoc-highlight--ignored {
|
|
29
|
+
opacity: 0.62;
|
|
30
|
+
border-bottom-style: dashed;
|
|
31
|
+
}
|