@ecomcx/ecomweb-content 0.0.0-stage → 1.14.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/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/block.d.ts +58 -0
- package/dist/block.js +60 -0
- package/dist/embed.d.ts +23 -0
- package/dist/embed.js +63 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +19 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +146 -0
- package/dist/schema.d.ts +31 -0
- package/dist/schema.js +167 -0
- package/dist/style.d.ts +74 -0
- package/dist/style.js +285 -0
- package/dist/text.d.ts +9 -0
- package/dist/text.js +30 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +6 -0
- package/dist/url.d.ts +7 -0
- package/dist/url.js +21 -0
- package/fixtures/all-core-nodes.json +782 -0
- package/fixtures/attr-coercion.json +219 -0
- package/fixtures/basic-blocks.json +414 -0
- package/fixtures/block-extras.json +131 -0
- package/fixtures/block-styles.json +316 -0
- package/fixtures/dynamic-blocks.json +316 -0
- package/fixtures/editor-output-nulls.json +112 -0
- package/fixtures/empty-and-newer.json +12 -0
- package/fixtures/invalid-structure.json +145 -0
- package/fixtures/layout-blocks.json +238 -0
- package/fixtures/legacy-html-string.json +17 -0
- package/fixtures/legacy-tiptap-doc.json +41 -0
- package/fixtures/plugin-blocks-preserved.json +155 -0
- package/fixtures/synced-pattern.json +80 -0
- package/package.json +44 -4
- package/style.css +161 -0
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mô tả thuộc tính của khối / định dạng chữ — dùng cho khối lõi dưới đây và cho khối của plugin (defineBlock).
|
|
3
|
+
* Giá trị sai kiểu được ép về đúng kiểu nếu được (số dạng chuỗi → số), không ép được thì bỏ (= giá trị mặc định).
|
|
4
|
+
* `null` và "không có" là một: thuộc tính null không được lưu.
|
|
5
|
+
*/
|
|
6
|
+
export type AttrType = "string" | "number" | "integer" | "boolean" | "url" | "json";
|
|
7
|
+
export interface AttrSpec {
|
|
8
|
+
type: AttrType;
|
|
9
|
+
/** Giá trị khi không có — trình soạn và website dùng chung. */
|
|
10
|
+
default?: unknown;
|
|
11
|
+
/** Chỉ nhận các giá trị này. */
|
|
12
|
+
enum?: readonly unknown[];
|
|
13
|
+
/** Số: kẹp trong [min, max]. */
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface NodeSpec {
|
|
18
|
+
attrs?: Record<string, AttrSpec>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Khối lõi trong nội dung soạn thảo (khối `richtext`). Thuộc tính không có trong danh sách vẫn được giữ nguyên
|
|
22
|
+
* (plugin có thể thêm thuộc tính vào khối lõi) — chỉ thuộc tính có mô tả mới được kiểm kiểu.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CORE_NODES: Record<string, NodeSpec>;
|
|
25
|
+
/** Khối động: nội dung lấy theo ngữ cảnh lúc hiển thị (website, trang, bài trong vòng lặp). */
|
|
26
|
+
export declare const DYNAMIC_NODES: readonly ["siteTitle", "siteLogo", "siteTagline", "navigation", "search", "languageSwitcher", "copyright", "postTitle", "postContent", "postExcerpt", "featuredImage", "postDate", "postAuthor", "postTerms", "comments", "query", "postTemplate", "pagination", "queryEmpty", "templatePart", "syncedPattern"];
|
|
27
|
+
export declare const CORE_MARKS: Record<string, NodeSpec>;
|
|
28
|
+
/** Ép giá trị theo mô tả. Trả về undefined = bỏ thuộc tính (dùng mặc định). */
|
|
29
|
+
export declare function coerceAttr(spec: AttrSpec, value: unknown): unknown;
|
|
30
|
+
/** Chuẩn hoá bộ thuộc tính: ép kiểu thuộc tính có mô tả, giữ nguyên thuộc tính khác, bỏ null. Rỗng → undefined. */
|
|
31
|
+
export declare function normalizeAttrs(attrs: unknown, spec?: Record<string, AttrSpec>): Record<string, unknown> | undefined;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const s = (extra = {}) => ({ type: "string", ...extra });
|
|
2
|
+
const n = (extra = {}) => ({ type: "number", ...extra });
|
|
3
|
+
const i = (extra = {}) => ({ type: "integer", ...extra });
|
|
4
|
+
const b = (extra = {}) => ({ type: "boolean", ...extra });
|
|
5
|
+
const url = () => ({ type: "url" });
|
|
6
|
+
const align = s({ enum: ["left", "center", "right", "justify"] });
|
|
7
|
+
const media = { src: url(), name: s(), mime: s(), mediaId: { type: "json" }, caption: s() };
|
|
8
|
+
/**
|
|
9
|
+
* Khối lõi trong nội dung soạn thảo (khối `richtext`). Thuộc tính không có trong danh sách vẫn được giữ nguyên
|
|
10
|
+
* (plugin có thể thêm thuộc tính vào khối lõi) — chỉ thuộc tính có mô tả mới được kiểm kiểu.
|
|
11
|
+
*/
|
|
12
|
+
export const CORE_NODES = {
|
|
13
|
+
doc: {},
|
|
14
|
+
text: {},
|
|
15
|
+
paragraph: { attrs: { textAlign: align } },
|
|
16
|
+
heading: { attrs: { level: i({ min: 1, max: 6, default: 2 }), textAlign: align, id: s() } },
|
|
17
|
+
bulletList: {},
|
|
18
|
+
orderedList: { attrs: { start: i({ min: 0, default: 1 }) } },
|
|
19
|
+
listItem: {},
|
|
20
|
+
taskList: {},
|
|
21
|
+
taskItem: { attrs: { checked: b({ default: false }) } },
|
|
22
|
+
blockquote: {},
|
|
23
|
+
codeBlock: { attrs: { language: s() } },
|
|
24
|
+
hardBreak: {},
|
|
25
|
+
horizontalRule: {},
|
|
26
|
+
image: {
|
|
27
|
+
attrs: {
|
|
28
|
+
src: url(), alt: s(), title: s(), width: n({ min: 1 }), height: n({ min: 1 }), srcset: s(), webpSrcset: s(),
|
|
29
|
+
mediaId: { type: "json" }, size: s({ default: "large" }), caption: s(), align: s({ enum: ["left", "right", "center", "wide", "full"] }),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
gallery: { attrs: { images: { type: "json", default: [] }, columns: i({ min: 1, max: 6, default: 3 }), caption: s() } },
|
|
33
|
+
video: { attrs: { ...media, poster: url() } },
|
|
34
|
+
audio: { attrs: { ...media } },
|
|
35
|
+
file: { attrs: { ...media, size: n({ min: 0 }), embed: b({ default: true }), download: b() } },
|
|
36
|
+
table: { attrs: { footer: b({ default: false }), fixed: b({ default: false }) } },
|
|
37
|
+
tableRow: {},
|
|
38
|
+
tableHeader: { attrs: { colspan: i({ min: 1, default: 1 }), rowspan: i({ min: 1, default: 1 }), colwidth: { type: "json" } } },
|
|
39
|
+
tableCell: { attrs: { colspan: i({ min: 1, default: 1 }), rowspan: i({ min: 1, default: 1 }), colwidth: { type: "json" } } },
|
|
40
|
+
youtube: { attrs: { src: url(), start: i({ min: 0 }), width: n({ min: 1 }), height: n({ min: 1 }) } },
|
|
41
|
+
// Bố cục (1.2): cột, nhóm, hộp lưu ý chứa khối khác; nút; nhúng theo đường dẫn (embedInfo)
|
|
42
|
+
columns: { attrs: { verticalAlign: s({ enum: ["top", "center", "bottom"] }), stack: b({ default: true }) } },
|
|
43
|
+
column: { attrs: { width: n({ min: 5, max: 100 }) } },
|
|
44
|
+
group: { attrs: { background: s(), textColor: s(), padding: s({ enum: ["none", "small", "medium", "large"], default: "medium" }) } },
|
|
45
|
+
callout: { attrs: { tone: s({ enum: ["info", "success", "warning", "danger"], default: "info" }), title: s() } },
|
|
46
|
+
button: {
|
|
47
|
+
attrs: {
|
|
48
|
+
text: s(), href: url(), target: s(), rel: s(),
|
|
49
|
+
variant: s({ enum: ["primary", "secondary", "outline"], default: "primary" }), align: s({ enum: ["left", "center", "right"] }),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
embed: { attrs: { url: url(), caption: s() } },
|
|
53
|
+
// Bộ khối cơ bản (1.9)
|
|
54
|
+
spacer: { attrs: { height: s({ default: "lg" }) } },
|
|
55
|
+
buttons: { attrs: { align: s({ enum: ["left", "center", "right", "space-between"] }) } },
|
|
56
|
+
cover: {
|
|
57
|
+
attrs: {
|
|
58
|
+
src: url(), mediaId: { type: "json" }, alt: s(), overlay: s(), opacity: i({ min: 0, max: 100, default: 50 }), minHeight: s({ default: "400px" }),
|
|
59
|
+
contentAlign: s({ enum: ["top", "center", "bottom"], default: "center" }), fixed: b({ default: false }),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
mediaText: {
|
|
63
|
+
attrs: {
|
|
64
|
+
src: url(), mediaId: { type: "json" }, alt: s(), position: s({ enum: ["left", "right"], default: "left" }), width: i({ min: 20, max: 80, default: 50 }),
|
|
65
|
+
verticalAlign: s({ enum: ["top", "center", "bottom"], default: "center" }), stack: b({ default: true }),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
pullquote: { attrs: { citation: s() } },
|
|
69
|
+
details: { attrs: { summary: s(), open: b({ default: false }) } },
|
|
70
|
+
customHtml: { attrs: { html: s() } },
|
|
71
|
+
socialLinks: { attrs: { links: { type: "json", default: [] }, size: s({ enum: ["small", "medium", "large"], default: "medium" }), align: s({ enum: ["left", "center", "right"] }), showLabels: b({ default: false }) } },
|
|
72
|
+
tableOfContents: { attrs: { title: s(), maxLevel: i({ min: 2, max: 4, default: 3 }) } },
|
|
73
|
+
preformatted: {},
|
|
74
|
+
// Khối động (1.6): lấy dữ liệu lúc hiển thị theo ngữ cảnh (website, trang đang xem, bài trong vòng lặp) — dùng trong template
|
|
75
|
+
siteTitle: { attrs: { level: i({ min: 0, max: 6, default: 0 }), link: b({ default: true }), textAlign: align } },
|
|
76
|
+
siteLogo: { attrs: { width: n({ min: 16, max: 600, default: 120 }), link: b({ default: true }) } },
|
|
77
|
+
siteTagline: { attrs: { textAlign: align } },
|
|
78
|
+
navigation: { attrs: { location: s({ default: "primary" }), orientation: s({ enum: ["horizontal", "vertical"], default: "horizontal" }), mobile: b({ default: true }) } },
|
|
79
|
+
search: { attrs: { placeholder: s(), buttonText: s() } },
|
|
80
|
+
languageSwitcher: {},
|
|
81
|
+
copyright: { attrs: { text: s(), textAlign: align } },
|
|
82
|
+
postTitle: { attrs: { level: i({ min: 1, max: 6, default: 1 }), link: b({ default: false }), textAlign: align } },
|
|
83
|
+
postContent: {},
|
|
84
|
+
postExcerpt: { attrs: { moreText: s() } },
|
|
85
|
+
featuredImage: { attrs: { size: s({ default: "large" }), link: b({ default: false }), aspect: s({ enum: ["auto", "16/9", "4/3", "1/1", "3/4"], default: "auto" }) } },
|
|
86
|
+
postDate: { attrs: { textAlign: align } },
|
|
87
|
+
postAuthor: { attrs: { avatar: b({ default: false }) } },
|
|
88
|
+
postTerms: { attrs: { taxonomy: s({ default: "category" }), separator: s({ default: ", " }) } },
|
|
89
|
+
comments: {},
|
|
90
|
+
query: {
|
|
91
|
+
attrs: {
|
|
92
|
+
inherit: b({ default: true }), postType: s({ default: "post" }), perPage: i({ min: 1, max: 50, default: 10 }),
|
|
93
|
+
category: s(), tag: s(), orderBy: s({ enum: ["date", "title", "menu_order"], default: "date" }), order: s({ enum: ["desc", "asc"], default: "desc" }),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
postTemplate: { attrs: { layout: s({ enum: ["list", "grid"], default: "list" }), columns: i({ min: 1, max: 6, default: 3 }) } },
|
|
97
|
+
pagination: {},
|
|
98
|
+
queryEmpty: {},
|
|
99
|
+
templatePart: { attrs: { slug: s(), tagName: s({ enum: ["div", "header", "footer", "aside", "section"], default: "div" }) } },
|
|
100
|
+
/** Mẫu đồng bộ: tham chiếu tới mẫu (id) — sửa mẫu gốc thì mọi chỗ dùng đổi theo. */
|
|
101
|
+
syncedPattern: { attrs: { ref: i({ min: 1 }) } },
|
|
102
|
+
};
|
|
103
|
+
/** Khối động: nội dung lấy theo ngữ cảnh lúc hiển thị (website, trang, bài trong vòng lặp). */
|
|
104
|
+
export const DYNAMIC_NODES = [
|
|
105
|
+
"siteTitle", "siteLogo", "siteTagline", "navigation", "search", "languageSwitcher", "copyright",
|
|
106
|
+
"postTitle", "postContent", "postExcerpt", "featuredImage", "postDate", "postAuthor", "postTerms", "comments",
|
|
107
|
+
"query", "postTemplate", "pagination", "queryEmpty", "templatePart", "syncedPattern",
|
|
108
|
+
];
|
|
109
|
+
export const CORE_MARKS = {
|
|
110
|
+
bold: {}, italic: {}, underline: {}, strike: {}, code: {}, subscript: {}, superscript: {},
|
|
111
|
+
highlight: { attrs: { color: s() } },
|
|
112
|
+
textStyle: { attrs: { color: s() } },
|
|
113
|
+
link: { attrs: { href: url(), target: s(), rel: s() } },
|
|
114
|
+
};
|
|
115
|
+
/** Ép giá trị theo mô tả. Trả về undefined = bỏ thuộc tính (dùng mặc định). */
|
|
116
|
+
export function coerceAttr(spec, value) {
|
|
117
|
+
if (value === null || value === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
let v = value;
|
|
120
|
+
switch (spec.type) {
|
|
121
|
+
case "string":
|
|
122
|
+
case "url":
|
|
123
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
124
|
+
v = String(v);
|
|
125
|
+
if (typeof v !== "string")
|
|
126
|
+
return undefined;
|
|
127
|
+
break;
|
|
128
|
+
case "number":
|
|
129
|
+
case "integer": {
|
|
130
|
+
const num = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
|
|
131
|
+
if (!Number.isFinite(num))
|
|
132
|
+
return undefined;
|
|
133
|
+
let x = spec.type === "integer" ? Math.round(num) : num;
|
|
134
|
+
if (spec.min !== undefined)
|
|
135
|
+
x = Math.max(spec.min, x);
|
|
136
|
+
if (spec.max !== undefined)
|
|
137
|
+
x = Math.min(spec.max, x);
|
|
138
|
+
v = x;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "boolean":
|
|
142
|
+
if (v === "true")
|
|
143
|
+
v = true;
|
|
144
|
+
else if (v === "false")
|
|
145
|
+
v = false;
|
|
146
|
+
if (typeof v !== "boolean")
|
|
147
|
+
return undefined;
|
|
148
|
+
break;
|
|
149
|
+
case "json":
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (spec.enum && !spec.enum.includes(v))
|
|
153
|
+
return undefined;
|
|
154
|
+
return v;
|
|
155
|
+
}
|
|
156
|
+
/** Chuẩn hoá bộ thuộc tính: ép kiểu thuộc tính có mô tả, giữ nguyên thuộc tính khác, bỏ null. Rỗng → undefined. */
|
|
157
|
+
export function normalizeAttrs(attrs, spec) {
|
|
158
|
+
if (!attrs || typeof attrs !== "object" || Array.isArray(attrs))
|
|
159
|
+
return undefined;
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
162
|
+
const v = spec?.[key] ? coerceAttr(spec[key], value) : value === null ? undefined : value;
|
|
163
|
+
if (v !== undefined)
|
|
164
|
+
out[key] = v;
|
|
165
|
+
}
|
|
166
|
+
return Object.keys(out).length ? out : undefined;
|
|
167
|
+
}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiểu dáng của khối (`attrs.style`, mọi khối): khoảng cách, cỡ chữ, màu, viền, ẩn theo khổ màn hình.
|
|
3
|
+
* Giá trị theo khổ: `{ base, tablet, mobile }` (tablet ≤ 1024px, mobile ≤ 640px). Giá trị là preset của theme
|
|
4
|
+
* (`lg`, `primary` → biến CSS `--ecx-spacing-lg`, `--ecx-color-primary`) hoặc giá trị CSS đã kiểm (độ dài, màu).
|
|
5
|
+
* Chỉ nhận giá trị trong danh sách → không chèn được CSS tuỳ ý. Máy chủ chạy cùng quy tắc (ContentFormat.cs).
|
|
6
|
+
*/
|
|
7
|
+
export declare const BREAKPOINTS: {
|
|
8
|
+
readonly tablet: 1024;
|
|
9
|
+
readonly mobile: 640;
|
|
10
|
+
};
|
|
11
|
+
export declare const DEVICES: readonly ["base", "tablet", "mobile"];
|
|
12
|
+
export type Device = (typeof DEVICES)[number];
|
|
13
|
+
export type Responsive<T> = Partial<Record<Device, T>>;
|
|
14
|
+
export type Box = string | Partial<Record<"top" | "right" | "bottom" | "left", string>>;
|
|
15
|
+
export interface BlockStyle {
|
|
16
|
+
padding?: Responsive<Box>;
|
|
17
|
+
margin?: Responsive<Box>;
|
|
18
|
+
gap?: Responsive<string>;
|
|
19
|
+
width?: Responsive<string>;
|
|
20
|
+
maxWidth?: Responsive<string>;
|
|
21
|
+
minHeight?: Responsive<string>;
|
|
22
|
+
fontFamily?: string;
|
|
23
|
+
fontSize?: Responsive<string>;
|
|
24
|
+
lineHeight?: Responsive<number>;
|
|
25
|
+
letterSpacing?: Responsive<string>;
|
|
26
|
+
fontWeight?: string;
|
|
27
|
+
fontStyle?: string;
|
|
28
|
+
textDecoration?: string;
|
|
29
|
+
textTransform?: string;
|
|
30
|
+
textAlign?: Responsive<string>;
|
|
31
|
+
color?: {
|
|
32
|
+
text?: string;
|
|
33
|
+
background?: string;
|
|
34
|
+
};
|
|
35
|
+
border?: {
|
|
36
|
+
width?: string;
|
|
37
|
+
style?: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
};
|
|
40
|
+
radius?: string;
|
|
41
|
+
hide?: Array<"desktop" | "tablet" | "mobile">;
|
|
42
|
+
}
|
|
43
|
+
/** Chuẩn hoá `attrs.style`: giữ đúng thứ tự khoá, bỏ giá trị không hợp lệ. Rỗng → undefined. */
|
|
44
|
+
export declare function normalizeStyle(v: unknown): BlockStyle | undefined;
|
|
45
|
+
/** Độ dài / preset khoảng cách hợp lệ → giá trị CSS (preset thành biến); không hợp lệ → undefined. Dùng cho thuộc tính khối như chiều cao khoảng trống. */
|
|
46
|
+
export declare function spacingValue(v: unknown): string | undefined;
|
|
47
|
+
/** Màu hợp lệ (hex/rgb/hsl hoặc preset) → giá trị CSS; không hợp lệ → undefined. */
|
|
48
|
+
export declare function colorCss(v: unknown): string | undefined;
|
|
49
|
+
/** Tên class ổn định theo nội dung style (cùng kiểu dáng → cùng class). */
|
|
50
|
+
export declare function styleClass(style: BlockStyle): string;
|
|
51
|
+
/** CSS của một kiểu dáng: `.cls.cls{…}` + media query cho tablet / mobile + ẩn theo khổ. Rỗng → "". */
|
|
52
|
+
export declare function styleCss(style: BlockStyle, cls?: string): string;
|
|
53
|
+
/** Gom CSS cho cả một cây nội dung: trả về css (mỗi class một lần) — website và trình soạn dùng chung. */
|
|
54
|
+
export declare function collectStyles(styles: Iterable<BlockStyle>): string;
|
|
55
|
+
/** Biến thể kiểu (`blockStyle`): tên preset → class `ecx-is-<tên>`. */
|
|
56
|
+
export declare function normalizeBlockStyleName(v: unknown): string | undefined;
|
|
57
|
+
/** Neo (`anchor`) → id của phần tử. */
|
|
58
|
+
export declare function normalizeAnchor(v: unknown): string | undefined;
|
|
59
|
+
/** Class CSS riêng (`className`): tối đa 5 class hợp lệ, cách nhau bởi dấu cách. */
|
|
60
|
+
export declare function normalizeClassName(v: unknown): string | undefined;
|
|
61
|
+
/** Biến thể có sẵn của lõi (CSS trong style.css); theme thêm bằng theme.json › blockStyles. */
|
|
62
|
+
export declare const CORE_BLOCK_STYLES: Record<string, string[]>;
|
|
63
|
+
export interface StylePresets {
|
|
64
|
+
colors?: Record<string, string>;
|
|
65
|
+
fontSizes?: Record<string, string>;
|
|
66
|
+
spacing?: Record<string, string>;
|
|
67
|
+
fonts?: Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
/** Chuẩn hoá preset: tên preset hợp lệ, giá trị đã kiểm (màu hex/rgb/hsl, độ dài hoặc clamp(), danh sách phông không có ; { }). */
|
|
70
|
+
export declare function normalizePresets(v: unknown): StylePresets;
|
|
71
|
+
/** Gộp preset theo thứ tự (sau đè trước): theme → bản sửa của quản trị viên. */
|
|
72
|
+
export declare function mergePresets(...list: unknown[]): StylePresets;
|
|
73
|
+
/** Biến CSS của preset: `--ecx-color-*`, `--ecx-font-size-*`, `--ecx-spacing-*`, `--ecx-font-family-*`. */
|
|
74
|
+
export declare function presetCss(presets: unknown, selector?: string): string;
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiểu dáng của khối (`attrs.style`, mọi khối): khoảng cách, cỡ chữ, màu, viền, ẩn theo khổ màn hình.
|
|
3
|
+
* Giá trị theo khổ: `{ base, tablet, mobile }` (tablet ≤ 1024px, mobile ≤ 640px). Giá trị là preset của theme
|
|
4
|
+
* (`lg`, `primary` → biến CSS `--ecx-spacing-lg`, `--ecx-color-primary`) hoặc giá trị CSS đã kiểm (độ dài, màu).
|
|
5
|
+
* Chỉ nhận giá trị trong danh sách → không chèn được CSS tuỳ ý. Máy chủ chạy cùng quy tắc (ContentFormat.cs).
|
|
6
|
+
*/
|
|
7
|
+
export const BREAKPOINTS = { tablet: 1024, mobile: 640 };
|
|
8
|
+
export const DEVICES = ["base", "tablet", "mobile"];
|
|
9
|
+
const SIDES = ["top", "right", "bottom", "left"];
|
|
10
|
+
const LENGTH = /^(0|-?\d{1,4}(\.\d{1,3})?(px|rem|em|%|vw|vh))$/;
|
|
11
|
+
const PRESET = /^([a-z][a-z0-9-]{0,30}|[1-9]x[sl])$/;
|
|
12
|
+
const COLOR = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\([\d\s.,%/]{1,40}\))$/i;
|
|
13
|
+
const WEIGHTS = ["300", "400", "500", "600", "700", "800", "900"];
|
|
14
|
+
const ALIGNS = ["left", "center", "right", "justify"];
|
|
15
|
+
const BORDER_STYLES = ["solid", "dashed", "dotted"];
|
|
16
|
+
const HIDE = ["desktop", "tablet", "mobile"];
|
|
17
|
+
const FONT_STYLES = ["normal", "italic"];
|
|
18
|
+
const DECORATIONS = ["none", "underline", "line-through"];
|
|
19
|
+
const TRANSFORMS = ["none", "uppercase", "lowercase", "capitalize"];
|
|
20
|
+
const preset = (v) => (typeof v === "string" && PRESET.test(v) ? v : undefined);
|
|
21
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
22
|
+
const size = (v) => (typeof v === "string" && (LENGTH.test(v) || PRESET.test(v)) ? v : undefined);
|
|
23
|
+
const color = (v) => (typeof v === "string" && (COLOR.test(v.trim()) || PRESET.test(v)) ? v.trim() : undefined);
|
|
24
|
+
const oneOf = (list) => (v) => (typeof v === "string" && list.includes(v) ? v : undefined);
|
|
25
|
+
const lineHeight = (v) => {
|
|
26
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
|
|
27
|
+
return Number.isFinite(n) && n >= 0.5 && n <= 4 ? Math.round(n * 100) / 100 : undefined;
|
|
28
|
+
};
|
|
29
|
+
function box(v) {
|
|
30
|
+
if (typeof v === "string")
|
|
31
|
+
return size(v);
|
|
32
|
+
if (!isObj(v))
|
|
33
|
+
return undefined;
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const s of SIDES) {
|
|
36
|
+
const x = size(v[s]);
|
|
37
|
+
if (x !== undefined)
|
|
38
|
+
out[s] = x;
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(out).length ? out : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Giá trị theo khổ: nhận cả dạng gọn (một giá trị = base). */
|
|
43
|
+
function responsive(v, fn) {
|
|
44
|
+
const src = isObj(v) && DEVICES.some((d) => d in v) ? v : { base: v };
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const d of DEVICES) {
|
|
47
|
+
const x = fn(src[d]);
|
|
48
|
+
if (x !== undefined)
|
|
49
|
+
out[d] = x;
|
|
50
|
+
}
|
|
51
|
+
return Object.keys(out).length ? out : undefined;
|
|
52
|
+
}
|
|
53
|
+
function group(v, fields) {
|
|
54
|
+
if (!isObj(v))
|
|
55
|
+
return undefined;
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const k of Object.keys(fields)) {
|
|
58
|
+
const x = fields[k](v[k]);
|
|
59
|
+
if (x !== undefined)
|
|
60
|
+
out[k] = x;
|
|
61
|
+
}
|
|
62
|
+
return Object.keys(out).length ? out : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Chuẩn hoá `attrs.style`: giữ đúng thứ tự khoá, bỏ giá trị không hợp lệ. Rỗng → undefined. */
|
|
65
|
+
export function normalizeStyle(v) {
|
|
66
|
+
if (!isObj(v))
|
|
67
|
+
return undefined;
|
|
68
|
+
const out = {};
|
|
69
|
+
const set = (k, x) => { if (x !== undefined)
|
|
70
|
+
out[k] = x; };
|
|
71
|
+
set("padding", responsive(v.padding, box));
|
|
72
|
+
set("margin", responsive(v.margin, box));
|
|
73
|
+
set("gap", responsive(v.gap, size));
|
|
74
|
+
set("width", responsive(v.width, size));
|
|
75
|
+
set("maxWidth", responsive(v.maxWidth, size));
|
|
76
|
+
set("minHeight", responsive(v.minHeight, size));
|
|
77
|
+
set("fontFamily", preset(v.fontFamily));
|
|
78
|
+
set("fontSize", responsive(v.fontSize, size));
|
|
79
|
+
set("lineHeight", responsive(v.lineHeight, lineHeight));
|
|
80
|
+
set("letterSpacing", responsive(v.letterSpacing, size));
|
|
81
|
+
set("fontWeight", oneOf(WEIGHTS)(typeof v.fontWeight === "number" ? String(v.fontWeight) : v.fontWeight));
|
|
82
|
+
set("fontStyle", oneOf(FONT_STYLES)(v.fontStyle));
|
|
83
|
+
set("textDecoration", oneOf(DECORATIONS)(v.textDecoration));
|
|
84
|
+
set("textTransform", oneOf(TRANSFORMS)(v.textTransform));
|
|
85
|
+
set("textAlign", responsive(v.textAlign, oneOf(ALIGNS)));
|
|
86
|
+
set("color", group(v.color, { text: color, background: color }));
|
|
87
|
+
set("border", group(v.border, { width: size, style: oneOf(BORDER_STYLES), color }));
|
|
88
|
+
set("radius", size(v.radius));
|
|
89
|
+
if (Array.isArray(v.hide)) {
|
|
90
|
+
const h = HIDE.filter((d) => v.hide.includes(d));
|
|
91
|
+
if (h.length)
|
|
92
|
+
out.hide = [...h];
|
|
93
|
+
}
|
|
94
|
+
return Object.keys(out).length ? out : undefined;
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// CSS
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const lit = (v) => v === "auto" || v === "0" || LENGTH.test(v);
|
|
100
|
+
const spacing = (v) => (lit(v) ? v : `var(--ecx-spacing-${v})`);
|
|
101
|
+
const fontSize = (v) => (lit(v) ? v : `var(--ecx-font-size-${v})`);
|
|
102
|
+
const colorValue = (v) => (COLOR.test(v) ? v : `var(--ecx-color-${v}, ${v})`);
|
|
103
|
+
function boxDecl(prop, v) {
|
|
104
|
+
if (typeof v === "string")
|
|
105
|
+
return [`${prop}:${spacing(v)}`];
|
|
106
|
+
return SIDES.filter((s) => v[s] !== undefined).map((s) => `${prop}-${s}:${spacing(v[s])}`);
|
|
107
|
+
}
|
|
108
|
+
/** Khai báo CSS cho từng khổ màn hình. */
|
|
109
|
+
function declarations(style) {
|
|
110
|
+
const out = { base: [], tablet: [], mobile: [] };
|
|
111
|
+
for (const d of DEVICES) {
|
|
112
|
+
const p = style.padding?.[d];
|
|
113
|
+
if (p !== undefined)
|
|
114
|
+
out[d].push(...boxDecl("padding", p));
|
|
115
|
+
const m = style.margin?.[d];
|
|
116
|
+
if (m !== undefined)
|
|
117
|
+
out[d].push(...boxDecl("margin", m));
|
|
118
|
+
const g = style.gap?.[d];
|
|
119
|
+
if (g !== undefined)
|
|
120
|
+
out[d].push(`gap:${spacing(g)}`);
|
|
121
|
+
const w = style.width?.[d];
|
|
122
|
+
if (w !== undefined)
|
|
123
|
+
out[d].push(`width:${spacing(w)}`);
|
|
124
|
+
const mw = style.maxWidth?.[d];
|
|
125
|
+
if (mw !== undefined)
|
|
126
|
+
out[d].push(`max-width:${spacing(mw)}`);
|
|
127
|
+
const mh = style.minHeight?.[d];
|
|
128
|
+
if (mh !== undefined)
|
|
129
|
+
out[d].push(`min-height:${spacing(mh)}`);
|
|
130
|
+
const fs = style.fontSize?.[d];
|
|
131
|
+
if (fs !== undefined)
|
|
132
|
+
out[d].push(`font-size:${fontSize(fs)}`);
|
|
133
|
+
const lh = style.lineHeight?.[d];
|
|
134
|
+
if (lh !== undefined)
|
|
135
|
+
out[d].push(`line-height:${lh}`);
|
|
136
|
+
const ls = style.letterSpacing?.[d];
|
|
137
|
+
if (ls !== undefined)
|
|
138
|
+
out[d].push(`letter-spacing:${lit(ls) ? ls : `var(--ecx-letter-spacing-${ls})`}`);
|
|
139
|
+
const ta = style.textAlign?.[d];
|
|
140
|
+
if (ta !== undefined)
|
|
141
|
+
out[d].push(`text-align:${ta}`);
|
|
142
|
+
}
|
|
143
|
+
if (style.fontFamily)
|
|
144
|
+
out.base.push(`font-family:var(--ecx-font-family-${style.fontFamily}, inherit)`);
|
|
145
|
+
if (style.fontWeight)
|
|
146
|
+
out.base.push(`font-weight:${style.fontWeight}`);
|
|
147
|
+
if (style.fontStyle)
|
|
148
|
+
out.base.push(`font-style:${style.fontStyle}`);
|
|
149
|
+
if (style.textDecoration)
|
|
150
|
+
out.base.push(`text-decoration:${style.textDecoration}`);
|
|
151
|
+
if (style.textTransform)
|
|
152
|
+
out.base.push(`text-transform:${style.textTransform}`);
|
|
153
|
+
if (style.color?.text)
|
|
154
|
+
out.base.push(`color:${colorValue(style.color.text)}`);
|
|
155
|
+
if (style.color?.background)
|
|
156
|
+
out.base.push(`background:${colorValue(style.color.background)}`);
|
|
157
|
+
if (style.border?.width || style.border?.style || style.border?.color)
|
|
158
|
+
out.base.push(`border:${style.border.width ? spacing(style.border.width) : "1px"} ${style.border.style ?? "solid"} ${style.border.color ? colorValue(style.border.color) : "currentColor"}`);
|
|
159
|
+
if (style.radius)
|
|
160
|
+
out.base.push(`border-radius:${spacing(style.radius)}`);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** Độ dài / preset khoảng cách hợp lệ → giá trị CSS (preset thành biến); không hợp lệ → undefined. Dùng cho thuộc tính khối như chiều cao khoảng trống. */
|
|
164
|
+
export function spacingValue(v) {
|
|
165
|
+
const x = size(v);
|
|
166
|
+
return x === undefined ? undefined : spacing(x);
|
|
167
|
+
}
|
|
168
|
+
/** Màu hợp lệ (hex/rgb/hsl hoặc preset) → giá trị CSS; không hợp lệ → undefined. */
|
|
169
|
+
export function colorCss(v) {
|
|
170
|
+
const x = color(v);
|
|
171
|
+
return x === undefined ? undefined : colorValue(x);
|
|
172
|
+
}
|
|
173
|
+
/** Tên class ổn định theo nội dung style (cùng kiểu dáng → cùng class). */
|
|
174
|
+
export function styleClass(style) {
|
|
175
|
+
let h = 0x811c9dc5;
|
|
176
|
+
const s = JSON.stringify(style);
|
|
177
|
+
for (let i = 0; i < s.length; i++) {
|
|
178
|
+
h ^= s.charCodeAt(i);
|
|
179
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
180
|
+
}
|
|
181
|
+
return `ecx-s-${h.toString(36)}`;
|
|
182
|
+
}
|
|
183
|
+
/** CSS của một kiểu dáng: `.cls.cls{…}` + media query cho tablet / mobile + ẩn theo khổ. Rỗng → "". */
|
|
184
|
+
export function styleCss(style, cls = styleClass(style)) {
|
|
185
|
+
const sel = `.${cls}.${cls}`;
|
|
186
|
+
const d = declarations(style);
|
|
187
|
+
let css = d.base.length ? `${sel}{${d.base.join(";")}}` : "";
|
|
188
|
+
if (d.tablet.length)
|
|
189
|
+
css += `@media (max-width:${BREAKPOINTS.tablet}px){${sel}{${d.tablet.join(";")}}}`;
|
|
190
|
+
if (d.mobile.length)
|
|
191
|
+
css += `@media (max-width:${BREAKPOINTS.mobile}px){${sel}{${d.mobile.join(";")}}}`;
|
|
192
|
+
for (const h of style.hide ?? []) {
|
|
193
|
+
const q = h === "desktop" ? `(min-width:${BREAKPOINTS.tablet + 1}px)` : h === "tablet" ? `(min-width:${BREAKPOINTS.mobile + 1}px) and (max-width:${BREAKPOINTS.tablet}px)` : `(max-width:${BREAKPOINTS.mobile}px)`;
|
|
194
|
+
css += `@media ${q}{${sel}{display:none!important}}`;
|
|
195
|
+
}
|
|
196
|
+
return css;
|
|
197
|
+
}
|
|
198
|
+
/** Gom CSS cho cả một cây nội dung: trả về css (mỗi class một lần) — website và trình soạn dùng chung. */
|
|
199
|
+
export function collectStyles(styles) {
|
|
200
|
+
const seen = new Map();
|
|
201
|
+
for (const s of styles) {
|
|
202
|
+
const cls = styleClass(s);
|
|
203
|
+
if (!seen.has(cls))
|
|
204
|
+
seen.set(cls, styleCss(s, cls));
|
|
205
|
+
}
|
|
206
|
+
return [...seen.values()].join("");
|
|
207
|
+
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Thuộc tính chung của khối (1.10): biến thể kiểu, neo, class riêng
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
const ANCHOR = /^[A-Za-z][\w-]{0,63}$/;
|
|
212
|
+
const CLASS_TOKEN = /^-?[_a-zA-Z][\w-]{0,40}$/;
|
|
213
|
+
/** Biến thể kiểu (`blockStyle`): tên preset → class `ecx-is-<tên>`. */
|
|
214
|
+
export function normalizeBlockStyleName(v) {
|
|
215
|
+
return typeof v === "string" && /^[a-z][a-z0-9-]{0,30}$/.test(v) ? v : undefined;
|
|
216
|
+
}
|
|
217
|
+
/** Neo (`anchor`) → id của phần tử. */
|
|
218
|
+
export function normalizeAnchor(v) {
|
|
219
|
+
return typeof v === "string" && ANCHOR.test(v.trim()) ? v.trim() : undefined;
|
|
220
|
+
}
|
|
221
|
+
/** Class CSS riêng (`className`): tối đa 5 class hợp lệ, cách nhau bởi dấu cách. */
|
|
222
|
+
export function normalizeClassName(v) {
|
|
223
|
+
if (typeof v !== "string")
|
|
224
|
+
return undefined;
|
|
225
|
+
const tokens = v.split(/\s+/).filter(Boolean).filter((t) => CLASS_TOKEN.test(t)).slice(0, 5);
|
|
226
|
+
return tokens.length ? [...new Set(tokens)].join(" ") : undefined;
|
|
227
|
+
}
|
|
228
|
+
/** Biến thể có sẵn của lõi (CSS trong style.css); theme thêm bằng theme.json › blockStyles. */
|
|
229
|
+
export const CORE_BLOCK_STYLES = {
|
|
230
|
+
table: ["stripes"],
|
|
231
|
+
image: ["rounded", "circle"],
|
|
232
|
+
blockquote: ["plain"],
|
|
233
|
+
horizontalRule: ["dots", "wide"],
|
|
234
|
+
button: ["pill"],
|
|
235
|
+
group: ["card"],
|
|
236
|
+
};
|
|
237
|
+
const FLUID = /^clamp\(\s*-?[\d.]+(px|rem|em|vw|vh|%)?\s*,\s*[\d.\s+\-*()a-z%]{1,60},\s*-?[\d.]+(px|rem|em|vw|vh|%)?\s*\)$/;
|
|
238
|
+
const FONT = /^[\w\s"',.\-]{1,200}$/;
|
|
239
|
+
const presetSize = (v) => (typeof v === "string" && (LENGTH.test(v.trim()) || FLUID.test(v.trim())) ? v.trim() : undefined);
|
|
240
|
+
const presetColor = (v) => (typeof v === "string" && COLOR.test(v.trim()) ? v.trim() : undefined);
|
|
241
|
+
const presetFont = (v) => (typeof v === "string" && FONT.test(v.trim()) ? v.trim() : undefined);
|
|
242
|
+
/** Chuẩn hoá preset: tên preset hợp lệ, giá trị đã kiểm (màu hex/rgb/hsl, độ dài hoặc clamp(), danh sách phông không có ; { }). */
|
|
243
|
+
export function normalizePresets(v) {
|
|
244
|
+
const out = {};
|
|
245
|
+
if (!isObj(v))
|
|
246
|
+
return out;
|
|
247
|
+
const pick = (key, fn) => {
|
|
248
|
+
const src = v[key];
|
|
249
|
+
if (!isObj(src))
|
|
250
|
+
return;
|
|
251
|
+
const r = {};
|
|
252
|
+
for (const [k, x] of Object.entries(src)) {
|
|
253
|
+
const value = isObj(x) ? x.value : x;
|
|
254
|
+
const ok = /^([a-z][a-z0-9-]{0,30}|[1-9]x[sl])$/.test(k) ? fn(value) : undefined;
|
|
255
|
+
if (ok !== undefined)
|
|
256
|
+
r[k] = ok;
|
|
257
|
+
}
|
|
258
|
+
if (Object.keys(r).length)
|
|
259
|
+
out[key] = r;
|
|
260
|
+
};
|
|
261
|
+
pick("colors", presetColor);
|
|
262
|
+
pick("fontSizes", presetSize);
|
|
263
|
+
pick("spacing", presetSize);
|
|
264
|
+
pick("fonts", presetFont);
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
/** Gộp preset theo thứ tự (sau đè trước): theme → bản sửa của quản trị viên. */
|
|
268
|
+
export function mergePresets(...list) {
|
|
269
|
+
const out = {};
|
|
270
|
+
for (const p of list.map(normalizePresets))
|
|
271
|
+
for (const k of Object.keys(p))
|
|
272
|
+
out[k] = { ...out[k], ...p[k] };
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
/** Biến CSS của preset: `--ecx-color-*`, `--ecx-font-size-*`, `--ecx-spacing-*`, `--ecx-font-family-*`. */
|
|
276
|
+
export function presetCss(presets, selector = ":root") {
|
|
277
|
+
const p = normalizePresets(presets);
|
|
278
|
+
const decl = [
|
|
279
|
+
...Object.entries(p.colors ?? {}).map(([k, v]) => `--ecx-color-${k}:${v}`),
|
|
280
|
+
...Object.entries(p.fontSizes ?? {}).map(([k, v]) => `--ecx-font-size-${k}:${v}`),
|
|
281
|
+
...Object.entries(p.spacing ?? {}).map(([k, v]) => `--ecx-spacing-${k}:${v}`),
|
|
282
|
+
...Object.entries(p.fonts ?? {}).map(([k, v]) => `--ecx-font-family-${k}:${v}`),
|
|
283
|
+
];
|
|
284
|
+
return decl.length ? `${selector}{${decl.join(";")}}` : "";
|
|
285
|
+
}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentDoc, RichNode } from "./types.js";
|
|
2
|
+
/** Chữ thuần của một nút (không định dạng). */
|
|
3
|
+
export declare function nodeText(node: RichNode): string;
|
|
4
|
+
/** Chữ thuần của cả bài (tìm kiếm, tóm tắt, đếm từ). Khối HTML bỏ thẻ; khối plugin bỏ qua. */
|
|
5
|
+
export declare function contentText(doc: ContentDoc): string;
|
|
6
|
+
/** Số từ (tách theo khoảng trắng — đúng cho tiếng Việt và tiếng Anh). */
|
|
7
|
+
export declare const wordCount: (doc: ContentDoc) => number;
|
|
8
|
+
/** Duyệt mọi nút soạn thảo của bài (kể cả lồng nhau). Trả false để không đi vào con. */
|
|
9
|
+
export declare function visitNodes(doc: ContentDoc, visit: (node: RichNode, parent: RichNode | null) => void | false): void;
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BLOCK_BREAK = new Set(["paragraph", "heading", "listItem", "taskItem", "blockquote", "codeBlock", "tableCell", "tableHeader"]);
|
|
2
|
+
/** Chữ thuần của một nút (không định dạng). */
|
|
3
|
+
export function nodeText(node) {
|
|
4
|
+
if (node.type === "text")
|
|
5
|
+
return node.text ?? "";
|
|
6
|
+
if (node.type === "hardBreak")
|
|
7
|
+
return "\n";
|
|
8
|
+
if (node.type === "button")
|
|
9
|
+
return typeof node.attrs?.text === "string" && node.attrs.text ? node.attrs.text + "\n" : "";
|
|
10
|
+
return (node.content ?? []).map(nodeText).join("") + (BLOCK_BREAK.has(node.type) ? "\n" : "");
|
|
11
|
+
}
|
|
12
|
+
/** Chữ thuần của cả bài (tìm kiếm, tóm tắt, đếm từ). Khối HTML bỏ thẻ; khối plugin bỏ qua. */
|
|
13
|
+
export function contentText(doc) {
|
|
14
|
+
return doc.blocks.map((b) => (b.type === "richtext" && b.data ? nodeText(b.data) : b.type === "html" ? String(b.data?.html ?? "").replace(/<[^>]+>/g, " ") : ""))
|
|
15
|
+
.join("\n").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
16
|
+
}
|
|
17
|
+
/** Số từ (tách theo khoảng trắng — đúng cho tiếng Việt và tiếng Anh). */
|
|
18
|
+
export const wordCount = (doc) => contentText(doc).split(/\s+/).filter(Boolean).length;
|
|
19
|
+
/** Duyệt mọi nút soạn thảo của bài (kể cả lồng nhau). Trả false để không đi vào con. */
|
|
20
|
+
export function visitNodes(doc, visit) {
|
|
21
|
+
const walk = (node, parent) => {
|
|
22
|
+
if (visit(node, parent) === false)
|
|
23
|
+
return;
|
|
24
|
+
for (const c of node.content ?? [])
|
|
25
|
+
walk(c, node);
|
|
26
|
+
};
|
|
27
|
+
for (const b of doc.blocks)
|
|
28
|
+
if (b.type === "richtext" && b.data)
|
|
29
|
+
walk(b.data, null);
|
|
30
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Định dạng nội dung của CMS (phiên bản 1). Đây là chuẩn của CMS — trình soạn (hiện là TipTap/ProseMirror) phải
|
|
3
|
+
* chuyển sang chuẩn này, website và máy chủ chỉ đọc chuẩn này. Đổi trình soạn không đổi dữ liệu đã lưu.
|
|
4
|
+
*/
|
|
5
|
+
/** Phiên bản định dạng hiện tại. Tăng khi đổi cấu trúc — kèm hàm nâng cấp trong migrate.ts. */
|
|
6
|
+
export declare const CONTENT_VERSION = 1;
|
|
7
|
+
/** Nội dung một bài viết / trang: danh sách khối cấp cao nhất. */
|
|
8
|
+
export interface ContentDoc {
|
|
9
|
+
version: number;
|
|
10
|
+
blocks: Block[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Khối cấp cao nhất.
|
|
14
|
+
* - `richtext`: `data` là cây {@link RichNode} gốc `type: "doc"` (nội dung soạn bằng trình soạn).
|
|
15
|
+
* - `html`: `data.html` là HTML (nội dung nhập từ hệ thống cũ).
|
|
16
|
+
* - tên khác: khối của plugin, `data` do plugin định nghĩa. Plugin không có/đã tắt → giữ nguyên dữ liệu, website bỏ qua.
|
|
17
|
+
*/
|
|
18
|
+
export interface Block {
|
|
19
|
+
id?: string;
|
|
20
|
+
type: string;
|
|
21
|
+
data: any;
|
|
22
|
+
}
|
|
23
|
+
/** Nút trong nội dung soạn thảo: khối (đoạn, tiêu đề, ảnh…) hoặc chữ (`type: "text"`). */
|
|
24
|
+
export interface RichNode {
|
|
25
|
+
type: string;
|
|
26
|
+
attrs?: Record<string, unknown>;
|
|
27
|
+
content?: RichNode[];
|
|
28
|
+
/** Chỉ có ở `type: "text"`, không rỗng. */
|
|
29
|
+
text?: string;
|
|
30
|
+
/** Định dạng chữ (đậm, liên kết…), chỉ có ở `type: "text"`. */
|
|
31
|
+
marks?: RichMark[];
|
|
32
|
+
}
|
|
33
|
+
export interface RichMark {
|
|
34
|
+
type: string;
|
|
35
|
+
attrs?: Record<string, unknown>;
|
|
36
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Định dạng nội dung của CMS (phiên bản 1). Đây là chuẩn của CMS — trình soạn (hiện là TipTap/ProseMirror) phải
|
|
3
|
+
* chuyển sang chuẩn này, website và máy chủ chỉ đọc chuẩn này. Đổi trình soạn không đổi dữ liệu đã lưu.
|
|
4
|
+
*/
|
|
5
|
+
/** Phiên bản định dạng hiện tại. Tăng khi đổi cấu trúc — kèm hàm nâng cấp trong migrate.ts. */
|
|
6
|
+
export const CONTENT_VERSION = 1;
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL an toàn cho href/src: http(s), mailto, tel, tương đối (/, #, ?, ./), ảnh data:image/* (không SVG).
|
|
3
|
+
* Cùng quy tắc với ContentSanitizer.IsSafeUrl của máy chủ.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isSafeUrl(url: unknown): boolean;
|
|
6
|
+
/** URL an toàn hoặc chuỗi thay thế. */
|
|
7
|
+
export declare const safeUrl: (url: unknown, fallback?: string) => string;
|