@growth-labs/cms 0.5.9 → 0.5.11
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 +23 -1
- package/dist/engine/content-insights.d.ts +11 -4
- package/dist/engine/content-insights.d.ts.map +1 -1
- package/dist/engine/content-insights.js +22 -7
- package/dist/engine/content-insights.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes/content-insights.d.ts +1 -1
- package/dist/routes/content-insights.d.ts.map +1 -1
- package/dist/routes/content-insights.js +20 -5
- package/dist/routes/content-insights.js.map +1 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +48 -48
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +9 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +67 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +10 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/surveys/schema.d.ts +48 -48
- package/dist/ui/editor/Rte.d.ts.map +1 -1
- package/dist/ui/editor/Rte.js +2 -12
- package/dist/ui/editor/Rte.js.map +1 -1
- package/dist/ui/editor/extensions.d.ts +3 -0
- package/dist/ui/editor/extensions.d.ts.map +1 -0
- package/dist/ui/editor/extensions.js +18 -0
- package/dist/ui/editor/extensions.js.map +1 -0
- package/dist/ui/editor/serialize.d.ts.map +1 -1
- package/dist/ui/editor/serialize.js +4 -2
- package/dist/ui/editor/serialize.js.map +1 -1
- package/dist/ui/editor/trim-boundary-marks.d.ts +11 -0
- package/dist/ui/editor/trim-boundary-marks.d.ts.map +1 -0
- package/dist/ui/editor/trim-boundary-marks.js +157 -0
- package/dist/ui/editor/trim-boundary-marks.js.map +1 -0
- package/dist/ui/screens/ContentInsightsScreen.d.ts +28 -0
- package/dist/ui/screens/ContentInsightsScreen.d.ts.map +1 -1
- package/dist/ui/screens/ContentInsightsScreen.js +111 -36
- package/dist/ui/screens/ContentInsightsScreen.js.map +1 -1
- package/dist/ui/screens/content-insights-data.d.ts +13 -1
- package/dist/ui/screens/content-insights-data.d.ts.map +1 -1
- package/dist/ui/screens/content-insights-data.js +29 -1
- package/dist/ui/screens/content-insights-data.js.map +1 -1
- package/migrations/0022_content_insight_dismissal_reasons.sql +8 -0
- package/package.json +1 -1
- package/src/engine/content-insights.ts +32 -7
- package/src/index.ts +2 -0
- package/src/routes/content-insights.ts +31 -5
- package/src/schema/index.ts +1 -0
- package/src/schema/migrations.ts +9 -0
- package/src/schema/types.ts +92 -0
- package/src/ui/editor/Rte.tsx +2 -12
- package/src/ui/editor/extensions.ts +18 -0
- package/src/ui/editor/serialize.ts +4 -2
- package/src/ui/editor/trim-boundary-marks.ts +181 -0
- package/src/ui/screens/ContentInsightsScreen.tsx +416 -195
- package/src/ui/screens/content-insights-data.ts +40 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Extension, type JSONContent } from '@tiptap/core'
|
|
2
|
+
import type { Mark, MarkType, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
3
|
+
import { Plugin } from '@tiptap/pm/state'
|
|
4
|
+
|
|
5
|
+
const BOUNDARY_MARKS = ['link', 'underline'] as const
|
|
6
|
+
|
|
7
|
+
function markKey(mark: NonNullable<JSONContent['marks']>[number]): string {
|
|
8
|
+
return `${mark.type}:${JSON.stringify(mark.attrs ?? {})}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sameMarks(
|
|
12
|
+
a: JSONContent['marks'] | undefined,
|
|
13
|
+
b: JSONContent['marks'] | undefined,
|
|
14
|
+
): boolean {
|
|
15
|
+
return JSON.stringify(a ?? []) === JSON.stringify(b ?? [])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mergeAdjacentText(nodes: JSONContent[]): JSONContent[] {
|
|
19
|
+
const merged: JSONContent[] = []
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
const previous = merged.at(-1)
|
|
22
|
+
if (
|
|
23
|
+
previous?.type === 'text' &&
|
|
24
|
+
node.type === 'text' &&
|
|
25
|
+
sameMarks(previous.marks, node.marks)
|
|
26
|
+
) {
|
|
27
|
+
previous.text = (previous.text ?? '') + (node.text ?? '')
|
|
28
|
+
} else {
|
|
29
|
+
merged.push(node)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return merged
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function trimJsonMark(nodes: JSONContent[], markType: (typeof BOUNDARY_MARKS)[number]) {
|
|
36
|
+
const output: JSONContent[] = []
|
|
37
|
+
let index = 0
|
|
38
|
+
|
|
39
|
+
while (index < nodes.length) {
|
|
40
|
+
const node = nodes[index]
|
|
41
|
+
const mark =
|
|
42
|
+
node.type === 'text'
|
|
43
|
+
? node.marks?.find((candidate) => candidate.type === markType)
|
|
44
|
+
: undefined
|
|
45
|
+
if (!mark) {
|
|
46
|
+
output.push(node)
|
|
47
|
+
index += 1
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = markKey(mark)
|
|
52
|
+
const run: JSONContent[] = []
|
|
53
|
+
while (index < nodes.length) {
|
|
54
|
+
const candidate = nodes[index]
|
|
55
|
+
const candidateMark =
|
|
56
|
+
candidate.type === 'text'
|
|
57
|
+
? candidate.marks?.find((item) => item.type === markType && markKey(item) === key)
|
|
58
|
+
: undefined
|
|
59
|
+
if (!candidateMark) break
|
|
60
|
+
run.push(candidate)
|
|
61
|
+
index += 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const text = run.map((item) => item.text ?? '').join('')
|
|
65
|
+
const leading = text.match(/^\s+/u)?.[0].length ?? 0
|
|
66
|
+
const trailing = text.match(/\s+$/u)?.[0].length ?? 0
|
|
67
|
+
const markedEnd = Math.max(leading, text.length - trailing)
|
|
68
|
+
let offset = 0
|
|
69
|
+
|
|
70
|
+
for (const item of run) {
|
|
71
|
+
const itemText = item.text ?? ''
|
|
72
|
+
const itemStart = offset
|
|
73
|
+
const itemEnd = offset + itemText.length
|
|
74
|
+
const cuts = [itemStart, Math.max(itemStart, leading), Math.min(itemEnd, markedEnd), itemEnd]
|
|
75
|
+
.filter(
|
|
76
|
+
(value, cutIndex, all) =>
|
|
77
|
+
value >= itemStart && value <= itemEnd && all.indexOf(value) === cutIndex,
|
|
78
|
+
)
|
|
79
|
+
.sort((a, b) => a - b)
|
|
80
|
+
|
|
81
|
+
for (let cutIndex = 0; cutIndex < cuts.length - 1; cutIndex += 1) {
|
|
82
|
+
const from = cuts[cutIndex]
|
|
83
|
+
const to = cuts[cutIndex + 1]
|
|
84
|
+
if (from === to) continue
|
|
85
|
+
const keepMark = from >= leading && to <= markedEnd
|
|
86
|
+
const marks = keepMark
|
|
87
|
+
? item.marks
|
|
88
|
+
: item.marks?.filter((candidate) => candidate.type !== markType)
|
|
89
|
+
output.push({
|
|
90
|
+
...item,
|
|
91
|
+
text: itemText.slice(from - itemStart, to - itemStart),
|
|
92
|
+
...(marks?.length ? { marks } : { marks: undefined }),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
offset = itemEnd
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return mergeAdjacentText(output)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove link and underline marks from leading/trailing whitespace in every
|
|
104
|
+
* inline mark run. Underline follows the same rule as links: whitespace is
|
|
105
|
+
* layout, not editorial emphasis, and pasted underline is the visible half of
|
|
106
|
+
* the malformed-link report.
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeBoundaryMarks(node: JSONContent): JSONContent {
|
|
109
|
+
if (!node.content) return node
|
|
110
|
+
let content = node.content.map(normalizeBoundaryMarks)
|
|
111
|
+
for (const markType of BOUNDARY_MARKS) content = trimJsonMark(content, markType)
|
|
112
|
+
return { ...node, content }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function matchingMark(
|
|
116
|
+
node: ProseMirrorNode,
|
|
117
|
+
markType: MarkType,
|
|
118
|
+
previous?: Mark,
|
|
119
|
+
): Mark | undefined {
|
|
120
|
+
const mark = markType.isInSet(node.marks)
|
|
121
|
+
if (!mark) return undefined
|
|
122
|
+
return previous && !mark.eq(previous) ? undefined : mark
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Keep the live Tiptap document visually consistent after toolbar and paste transactions. */
|
|
126
|
+
export const TrimBoundaryMarks = Extension.create({
|
|
127
|
+
name: 'trimBoundaryMarks',
|
|
128
|
+
|
|
129
|
+
addProseMirrorPlugins() {
|
|
130
|
+
return [
|
|
131
|
+
new Plugin({
|
|
132
|
+
appendTransaction: (transactions, _oldState, newState) => {
|
|
133
|
+
if (!transactions.some((transaction) => transaction.docChanged)) return null
|
|
134
|
+
const transaction = newState.tr
|
|
135
|
+
|
|
136
|
+
newState.doc.descendants((parent, parentPosition) => {
|
|
137
|
+
if (!parent.isTextblock) return true
|
|
138
|
+
|
|
139
|
+
for (const markName of BOUNDARY_MARKS) {
|
|
140
|
+
const markType = newState.schema.marks[markName]
|
|
141
|
+
if (!markType) continue
|
|
142
|
+
const children: Array<{ node: ProseMirrorNode; from: number }> = []
|
|
143
|
+
parent.forEach((child, offset) => {
|
|
144
|
+
if (child.isText) children.push({ node: child, from: parentPosition + 1 + offset })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
let index = 0
|
|
148
|
+
while (index < children.length) {
|
|
149
|
+
const first = children[index]
|
|
150
|
+
const mark = matchingMark(first.node, markType)
|
|
151
|
+
if (!mark) {
|
|
152
|
+
index += 1
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const run: typeof children = []
|
|
157
|
+
while (
|
|
158
|
+
index < children.length &&
|
|
159
|
+
matchingMark(children[index].node, markType, mark)
|
|
160
|
+
) {
|
|
161
|
+
run.push(children[index])
|
|
162
|
+
index += 1
|
|
163
|
+
}
|
|
164
|
+
const text = run.map((item) => item.node.text ?? '').join('')
|
|
165
|
+
const leading = text.match(/^\s+/u)?.[0].length ?? 0
|
|
166
|
+
const trailing = text.match(/\s+$/u)?.[0].length ?? 0
|
|
167
|
+
const runFrom = run[0].from
|
|
168
|
+
const runTo = run.at(-1)!.from + (run.at(-1)!.node.text?.length ?? 0)
|
|
169
|
+
if (leading) transaction.removeMark(runFrom, runFrom + leading, markType)
|
|
170
|
+
if (trailing) transaction.removeMark(runTo - trailing, runTo, markType)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
return transaction.steps.length ? transaction : null
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
})
|