wendang 0.1.0 → 0.2.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 +43 -2
- package/NOTICE.md +1 -1
- package/README.md +120 -52
- package/dist/cjs/document.js +1 -19
- package/dist/cjs/index.js +1 -25
- package/dist/cjs/mindmap.js +1 -22
- package/dist/cjs/schema.js +1 -11
- package/dist/cjs/shared/anchor.js +1 -201
- package/dist/cjs/shared/callout.js +1 -64
- package/dist/cjs/shared/code-block.js +1 -33
- package/dist/cjs/shared/code-languages.js +1 -51
- package/dist/cjs/shared/doc-compatibility.js +1 -23
- package/dist/cjs/shared/doc-text.js +1 -52
- package/dist/cjs/shared/document-colors.js +1 -61
- package/dist/cjs/shared/headings.js +1 -0
- package/dist/cjs/shared/inline-comments.js +1 -179
- package/dist/cjs/shared/mind-map-folding.js +1 -14
- package/dist/cjs/shared/mind-map-phase2.js +1 -133
- package/dist/cjs/shared/mind-map.js +1 -445
- package/dist/cjs/shared/mind-topic-text.js +1 -57
- package/dist/cjs/shared/node-catalog.js +1 -62
- package/dist/cjs/shared/presence.js +1 -141
- package/dist/cjs/shared/safe-href.js +1 -36
- package/dist/cjs/shared/table-cell-format.js +1 -18
- package/dist/cjs/shared/table-layout.js +1 -28
- package/dist/cjs/shared/types.js +1 -2
- package/dist/cjs/shared/video-embed.js +1 -102
- package/dist/cjs/shared/workbook.js +1 -152
- package/dist/cjs/spreadsheet.js +1 -17
- package/dist/cjs/types.js +1 -17
- package/dist/cjs/validation/callout.js +1 -17
- package/dist/cjs/validation/doc-node-whitelist.js +1 -71
- package/dist/cjs/validation/error.js +1 -17
- package/dist/cjs/validation/mind-map.js +1 -129
- package/dist/cjs/validation/schema-version.js +1 -5
- package/dist/cjs/validation/schema.js +1 -376
- package/dist/cjs/validation/video-embed.js +1 -17
- package/dist/cjs/validation/workbook.js +1 -17
- package/dist/esm/document.js +1 -3
- package/dist/esm/index.js +1 -3
- package/dist/esm/mindmap.js +1 -4
- package/dist/esm/schema.js +1 -3
- package/dist/esm/shared/anchor.js +1 -192
- package/dist/esm/shared/callout.js +1 -56
- package/dist/esm/shared/code-block.js +1 -25
- package/dist/esm/shared/code-languages.js +1 -46
- package/dist/esm/shared/doc-compatibility.js +1 -20
- package/dist/esm/shared/doc-text.js +1 -48
- package/dist/esm/shared/document-colors.js +1 -56
- package/dist/esm/shared/headings.js +1 -0
- package/dist/esm/shared/inline-comments.js +1 -173
- package/dist/esm/shared/mind-map-folding.js +1 -10
- package/dist/esm/shared/mind-map-phase2.js +1 -121
- package/dist/esm/shared/mind-map.js +1 -427
- package/dist/esm/shared/mind-topic-text.js +1 -49
- package/dist/esm/shared/node-catalog.js +1 -57
- package/dist/esm/shared/presence.js +1 -130
- package/dist/esm/shared/safe-href.js +1 -32
- package/dist/esm/shared/table-cell-format.js +1 -14
- package/dist/esm/shared/table-layout.js +1 -23
- package/dist/esm/shared/types.js +1 -1
- package/dist/esm/shared/video-embed.js +1 -96
- package/dist/esm/shared/workbook.js +1 -144
- package/dist/esm/spreadsheet.js +1 -1
- package/dist/esm/types.js +1 -1
- package/dist/esm/validation/callout.js +1 -1
- package/dist/esm/validation/doc-node-whitelist.js +1 -68
- package/dist/esm/validation/error.js +1 -13
- package/dist/esm/validation/mind-map.js +1 -125
- package/dist/esm/validation/schema-version.js +1 -2
- package/dist/esm/validation/schema.js +1 -372
- package/dist/esm/validation/video-embed.js +1 -1
- package/dist/esm/validation/workbook.js +1 -1
- package/dist/types/document.d.ts +1 -0
- package/dist/types/shared/anchor.d.ts +0 -55
- package/dist/types/shared/callout.d.ts +39 -2
- package/dist/types/shared/code-block.d.ts +0 -1
- package/dist/types/shared/code-languages.d.ts +0 -19
- package/dist/types/shared/doc-text.d.ts +0 -14
- package/dist/types/shared/document-colors.d.ts +0 -4
- package/dist/types/shared/headings.d.ts +7 -0
- package/dist/types/shared/inline-comments.d.ts +0 -42
- package/dist/types/shared/mind-map-folding.d.ts +0 -2
- package/dist/types/shared/mind-map.d.ts +0 -4
- package/dist/types/shared/mind-topic-text.d.ts +0 -2
- package/dist/types/shared/node-catalog.d.ts +0 -17
- package/dist/types/shared/presence.d.ts +0 -51
- package/dist/types/shared/safe-href.d.ts +0 -1
- package/dist/types/shared/table-cell-format.d.ts +0 -1
- package/dist/types/shared/table-layout.d.ts +0 -3
- package/dist/types/shared/types.d.ts +0 -4
- package/dist/types/shared/video-embed.d.ts +9 -2
- package/dist/types/shared/workbook.d.ts +0 -2
- package/dist/types/validation/doc-node-whitelist.d.ts +0 -19
- package/dist/types/validation/mind-map.d.ts +0 -2
- package/dist/types/validation/schema-version.d.ts +0 -1
- package/dist/types/validation/schema.d.ts +0 -34
- package/package.json +9 -9
|
@@ -1,57 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MIND_TOPIC_FONT = exports.MIND_TOPIC_PADDING = exports.mindTopicLineHeight = exports.MIND_TOPIC_LINE_HEIGHT = void 0;
|
|
4
|
-
exports.setMindTextMeasureFactory = setMindTextMeasureFactory;
|
|
5
|
-
exports.mindTextMeasure = mindTextMeasure;
|
|
6
|
-
exports.wrapMindText = wrapMindText;
|
|
7
|
-
exports.mindTopicTextSize = mindTopicTextSize;
|
|
8
|
-
exports.MIND_TOPIC_LINE_HEIGHT = 22;
|
|
9
|
-
const mindTopicLineHeight = (fontSize = 14) => Math.max(exports.MIND_TOPIC_LINE_HEIGHT, Math.ceil(fontSize * 1.4));
|
|
10
|
-
exports.mindTopicLineHeight = mindTopicLineHeight;
|
|
11
|
-
exports.MIND_TOPIC_PADDING = 34; // 16px * 2 padding + 1px * 2 borders.
|
|
12
|
-
exports.MIND_TOPIC_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
|
|
13
|
-
const sizeCache = new Map();
|
|
14
|
-
let factory;
|
|
15
|
-
function setMindTextMeasureFactory(value) { factory = value; sizeCache.clear(); }
|
|
16
|
-
/** Real browser font metrics; non-DOM exports/tests keep a conservative text-only fallback. */
|
|
17
|
-
function mindTextMeasure(fontSize = 14, weight = 400) {
|
|
18
|
-
const measured = factory?.(fontSize, weight);
|
|
19
|
-
if (measured)
|
|
20
|
-
return measured;
|
|
21
|
-
return text => [...text].reduce((sum, ch) => sum + (/[\u0000-\u00ff]/.test(ch) ? 9 : 14) * fontSize / 14, 0);
|
|
22
|
-
}
|
|
23
|
-
/** Keep explicit empty lines and emoji clusters; presentation wrapping never edits the source. */
|
|
24
|
-
function wrapMindText(text, width, measure) {
|
|
25
|
-
const lines = [];
|
|
26
|
-
const segmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
27
|
-
for (const line of text.split('\n')) {
|
|
28
|
-
let part = '';
|
|
29
|
-
const segments = segmenter ? [...segmenter.segment(line)].map(s => s.segment) : [...line];
|
|
30
|
-
for (const ch of segments) {
|
|
31
|
-
if (part && measure(part + ch) > width) {
|
|
32
|
-
lines.push(part);
|
|
33
|
-
part = '';
|
|
34
|
-
}
|
|
35
|
-
part += ch;
|
|
36
|
-
}
|
|
37
|
-
lines.push(part);
|
|
38
|
-
}
|
|
39
|
-
return lines;
|
|
40
|
-
}
|
|
41
|
-
function mindTopicTextSize(text, fontSize = 14, root = false, description = '', bold = false, minimumWidth = 0) {
|
|
42
|
-
const key = JSON.stringify([text, fontSize, root, description, bold, minimumWidth]);
|
|
43
|
-
const cached = sizeCache.get(key);
|
|
44
|
-
if (cached)
|
|
45
|
-
return cached;
|
|
46
|
-
const measure = mindTextMeasure(fontSize, bold ? 700 : root ? 600 : 400), descriptionMeasure = mindTextMeasure(12);
|
|
47
|
-
const source = text || '未命名主题';
|
|
48
|
-
const longest = Math.max(...source.split('\n').map(measure), ...description.split('\n').map(descriptionMeasure));
|
|
49
|
-
const width = Math.min(240, Math.max(root ? 128 : 100, minimumWidth, Math.ceil(longest + exports.MIND_TOPIC_PADDING + 2)));
|
|
50
|
-
const lines = wrapMindText(source, width - exports.MIND_TOPIC_PADDING, measure);
|
|
51
|
-
const descriptionLines = description ? wrapMindText(description, width - exports.MIND_TOPIC_PADDING, descriptionMeasure) : [];
|
|
52
|
-
const result = { width, lines, descriptionLines, titleHeight: lines.length * (0, exports.mindTopicLineHeight)(fontSize) };
|
|
53
|
-
if (sizeCache.size >= 512)
|
|
54
|
-
sizeCache.delete(sizeCache.keys().next().value);
|
|
55
|
-
sizeCache.set(key, result);
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.MIND_TOPIC_FONT=exports.MIND_TOPIC_PADDING=exports.mindTopicLineHeight=exports.MIND_TOPIC_LINE_HEIGHT=void 0,exports.setMindTextMeasureFactory=function setMindTextMeasureFactory(n){t=n,e.clear()},exports.mindTextMeasure=mindTextMeasure,exports.wrapMindText=wrapMindText,exports.mindTopicTextSize=function mindTopicTextSize(t,n=14,i=!1,s="",r=!1,o=0){const p=JSON.stringify([t,n,i,s,r,o]),a=e.get(p);if(a)return a;const x=mindTextMeasure(n,r?700:i?600:400),I=mindTextMeasure(12),M=t||"未命名主题",T=Math.max(...M.split("\n").map(x),...s.split("\n").map(I)),c=Math.min(240,Math.max(i?128:100,o,Math.ceil(T+exports.MIND_TOPIC_PADDING+2))),u=wrapMindText(M,c-exports.MIND_TOPIC_PADDING,x),d={width:c,lines:u,descriptionLines:s?wrapMindText(s,c-exports.MIND_TOPIC_PADDING,I):[],titleHeight:u.length*(0,exports.mindTopicLineHeight)(n)};return e.size>=512&&e.delete(e.keys().next().value),e.set(p,d),d},exports.MIND_TOPIC_LINE_HEIGHT=22,exports.mindTopicLineHeight=(e=14)=>Math.max(exports.MIND_TOPIC_LINE_HEIGHT,Math.ceil(1.4*e)),exports.MIND_TOPIC_PADDING=34,exports.MIND_TOPIC_FONT='-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';const e=new Map;let t;function mindTextMeasure(e=14,n=400){const i=t?.(e,n);return i||(t=>[...t].reduce((t,n)=>t+(/[\u0000-\u00ff]/.test(n)?9:14)*e/14,0))}function wrapMindText(e,t,n){const i=[],s="function"==typeof Intl.Segmenter?new Intl.Segmenter(void 0,{granularity:"grapheme"}):null;for(const r of e.split("\n")){let e="";const o=s?[...s.segment(r)].map(e=>e.segment):[...r];for(const s of o)e&&n(e+s)>t&&(i.push(e),e=""),e+=s;i.push(e)}return i}
|
|
@@ -1,62 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 节点目录:编辑器的 nodeView 与只读渲染规则**共用的单一真源**
|
|
4
|
-
* (对齐《Wiki好用技术方案》1.1)。
|
|
5
|
-
*
|
|
6
|
-
* 这里只放纯数据(无 Vue、无 Tiptap 依赖),因此阅读端可以只引它 + Renderer,
|
|
7
|
-
* 而完全不加载编辑器代码;编辑器端再基于同一份目录生成扩展。
|
|
8
|
-
*
|
|
9
|
-
* 新增节点时必须同时更新:
|
|
10
|
-
* 1) 本目录 2) 服务端 doc-schema.service.ts 白名单 3) 提升 schema 版本
|
|
11
|
-
* 否则旧客户端会因「遇到不支持节点」而被服务端拒写(这是预期行为,避免静默丢内容)。
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.UNKNOWN_NODE_LABEL = exports.NODE_CATALOG = void 0;
|
|
15
|
-
exports.nodeMeta = nodeMeta;
|
|
16
|
-
exports.headingTag = headingTag;
|
|
17
|
-
exports.NODE_CATALOG = [
|
|
18
|
-
{ type: 'doc', kind: 'block', label: '文档', topLevel: true },
|
|
19
|
-
{ type: 'paragraph', kind: 'block', label: '段落', topLevel: true },
|
|
20
|
-
{ type: 'heading', kind: 'block', label: '标题', topLevel: true },
|
|
21
|
-
{ type: 'bulletList', kind: 'block', label: '无序列表', topLevel: true },
|
|
22
|
-
{ type: 'orderedList', kind: 'block', label: '有序列表', topLevel: true },
|
|
23
|
-
{ type: 'taskList', kind: 'block', label: '待办列表', topLevel: true },
|
|
24
|
-
{ type: 'listItem', kind: 'block', label: '列表项' },
|
|
25
|
-
{ type: 'taskItem', kind: 'block', label: '待办项' },
|
|
26
|
-
{ type: 'blockquote', kind: 'block', label: '引用块', topLevel: true },
|
|
27
|
-
{ type: 'codeBlock', kind: 'block', label: '代码块', topLevel: true },
|
|
28
|
-
{ type: 'horizontalRule', kind: 'atom', atom: true, label: '分割线', topLevel: true },
|
|
29
|
-
{ type: 'table', kind: 'block', label: '表格', topLevel: true },
|
|
30
|
-
{ type: 'tableRow', kind: 'block', label: '表格行' },
|
|
31
|
-
{ type: 'tableHeader', kind: 'block', label: '表头单元格' },
|
|
32
|
-
{ type: 'tableCell', kind: 'block', label: '单元格' },
|
|
33
|
-
{ type: 'image', kind: 'atom', atom: true, label: '图片', topLevel: true },
|
|
34
|
-
{ type: 'fileAttachment', kind: 'atom', atom: true, label: '视频或文件', topLevel: true },
|
|
35
|
-
{ type: 'videoEmbed', kind: 'atom', atom: true, label: '嵌入视频', topLevel: true },
|
|
36
|
-
{ type: 'mindMap', kind: 'block', label: '思维导图', topLevel: true },
|
|
37
|
-
{ type: 'mindMapDescription', kind: 'inline', label: '主题描述' },
|
|
38
|
-
{ type: 'mindMapNote', kind: 'inline', label: '主题备注' },
|
|
39
|
-
{ type: 'mindMapRelation', kind: 'block', label: '主题关系' },
|
|
40
|
-
{ type: 'mindMapTopic', kind: 'block', label: '导图主题' },
|
|
41
|
-
{ type: 'callout', kind: 'block', label: '高亮块', topLevel: true },
|
|
42
|
-
// 分栏:columns 是顶层行,column 只在其内部(故 column 无 topLevel)
|
|
43
|
-
{ type: 'columns', kind: 'block', label: '分栏', topLevel: true },
|
|
44
|
-
{ type: 'column', kind: 'block', label: '栏' },
|
|
45
|
-
{ type: 'pageLink', kind: 'atom', atom: true, label: '页面引用' },
|
|
46
|
-
{ type: 'mention', kind: 'atom', atom: true, label: '提及用户' },
|
|
47
|
-
{ type: 'mathInline', kind: 'atom', atom: true, label: '行内公式' },
|
|
48
|
-
{ type: 'mathBlock', kind: 'atom', atom: true, label: '公式块', topLevel: true },
|
|
49
|
-
{ type: 'text', kind: 'text', label: '文本' },
|
|
50
|
-
{ type: 'hardBreak', kind: 'inline', atom: true, label: '换行' },
|
|
51
|
-
];
|
|
52
|
-
const META_BY_TYPE = new Map(exports.NODE_CATALOG.map((n) => [n.type, n]));
|
|
53
|
-
function nodeMeta(type) {
|
|
54
|
-
return type ? META_BY_TYPE.get(type) : undefined;
|
|
55
|
-
}
|
|
56
|
-
/** 只读渲染遇到未知节点时使用的降级提示(不静默丢弃,向用户明示)。 */
|
|
57
|
-
exports.UNKNOWN_NODE_LABEL = '此内容由更高版本的编辑器创建,当前版本无法显示';
|
|
58
|
-
/** 标题级别 → HTML 标签 */
|
|
59
|
-
function headingTag(level) {
|
|
60
|
-
const n = typeof level === 'number' ? level : 1;
|
|
61
|
-
return `h${Math.min(Math.max(n, 1), 6)}`;
|
|
62
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.UNKNOWN_NODE_LABEL=exports.NODE_CATALOG=void 0,exports.nodeMeta=function nodeMeta(l){return l?e.get(l):void 0},exports.headingTag=function headingTag(e){const l="number"==typeof e?e:1;return`h${Math.min(Math.max(l,1),6)}`},exports.NODE_CATALOG=[{type:"doc",kind:"block",label:"文档",topLevel:!0},{type:"paragraph",kind:"block",label:"段落",topLevel:!0},{type:"heading",kind:"block",label:"标题",topLevel:!0},{type:"bulletList",kind:"block",label:"无序列表",topLevel:!0},{type:"orderedList",kind:"block",label:"有序列表",topLevel:!0},{type:"taskList",kind:"block",label:"待办列表",topLevel:!0},{type:"listItem",kind:"block",label:"列表项"},{type:"taskItem",kind:"block",label:"待办项"},{type:"blockquote",kind:"block",label:"引用块",topLevel:!0},{type:"codeBlock",kind:"block",label:"代码块",topLevel:!0},{type:"horizontalRule",kind:"atom",atom:!0,label:"分割线",topLevel:!0},{type:"table",kind:"block",label:"表格",topLevel:!0},{type:"tableRow",kind:"block",label:"表格行"},{type:"tableHeader",kind:"block",label:"表头单元格"},{type:"tableCell",kind:"block",label:"单元格"},{type:"image",kind:"atom",atom:!0,label:"图片",topLevel:!0},{type:"fileAttachment",kind:"atom",atom:!0,label:"视频或文件",topLevel:!0},{type:"videoEmbed",kind:"atom",atom:!0,label:"嵌入视频",topLevel:!0},{type:"mindMap",kind:"block",label:"思维导图",topLevel:!0},{type:"mindMapDescription",kind:"inline",label:"主题描述"},{type:"mindMapNote",kind:"inline",label:"主题备注"},{type:"mindMapRelation",kind:"block",label:"主题关系"},{type:"mindMapTopic",kind:"block",label:"导图主题"},{type:"callout",kind:"block",label:"高亮块",topLevel:!0},{type:"columns",kind:"block",label:"分栏",topLevel:!0},{type:"column",kind:"block",label:"栏"},{type:"pageLink",kind:"atom",atom:!0,label:"页面引用"},{type:"mention",kind:"atom",atom:!0,label:"提及用户"},{type:"mathInline",kind:"atom",atom:!0,label:"行内公式"},{type:"mathBlock",kind:"atom",atom:!0,label:"公式块",topLevel:!0},{type:"text",kind:"text",label:"文本"},{type:"hardBreak",kind:"inline",atom:!0,label:"换行"}];const e=new Map(exports.NODE_CATALOG.map(e=>[e.type,e]));exports.UNKNOWN_NODE_LABEL="此内容由更高版本的编辑器创建,当前版本无法显示";
|
|
@@ -1,141 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 协同在线状态(presence)的纯逻辑(可单测,不依赖 DOM / Yjs / 网络)。
|
|
4
|
-
*
|
|
5
|
-
* ## 为什么单独抽出来
|
|
6
|
-
*
|
|
7
|
-
* 在线状态有两个容易做错、且出错后**不报错、只是显示不对**的地方:
|
|
8
|
-
* 1) 颜色分配:同一用户在不同客户端必须得到**相同颜色**(否则"那个蓝色光标是谁"
|
|
9
|
-
* 无法对应),而颜色若只按"连接顺序"分,B 客户端上同一个人的颜色就会变。
|
|
10
|
-
* 因此颜色必须由**用户身份**稳定推导,不能依赖顺序或随机。
|
|
11
|
-
* 2) 对比度:名字气泡上的文字要与背景色对比可读;简单地对 6 个色相取固定亮度,
|
|
12
|
-
* 在浅色模式下会有几个色相偏亮、白字看不清。
|
|
13
|
-
* 这些都能用纯函数锁定,因此放在这里。
|
|
14
|
-
*/
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.DARK_TEXT = exports.PRESENCE_PALETTE = void 0;
|
|
17
|
-
exports.colorForUser = colorForUser;
|
|
18
|
-
exports.hashIndex = hashIndex;
|
|
19
|
-
exports.initialOf = initialOf;
|
|
20
|
-
exports.collectPresence = collectPresence;
|
|
21
|
-
exports.relativeLuminance = relativeLuminance;
|
|
22
|
-
exports.contrastRatio = contrastRatio;
|
|
23
|
-
exports.readableTextColor = readableTextColor;
|
|
24
|
-
exports.parseHex = parseHex;
|
|
25
|
-
/**
|
|
26
|
-
* 稳定的用户色板。
|
|
27
|
-
*
|
|
28
|
-
* 选取标准不是"好看",而是**所有颜色配白字都达到 WCAG AA(≥4.5:1)**——
|
|
29
|
-
* 这样光标/名字气泡永远是"彩色底 + 白字",视觉一致,也不会出现浅底白字看不清。
|
|
30
|
-
* (初版用了偏亮的绿 `#1a9d5a`(3.49:1)与橙 `#d97706`(3.19:1),配白字或深字
|
|
31
|
-
* 都不到 AA;这里换成更深的一档,由单测锁定对比度。)
|
|
32
|
-
*/
|
|
33
|
-
exports.PRESENCE_PALETTE = [
|
|
34
|
-
'#2f6feb', // 蓝 4.57:1
|
|
35
|
-
'#127a45', // 绿 5.39:1
|
|
36
|
-
'#b45309', // 棕橙 5.02:1
|
|
37
|
-
'#c026d3', // 紫 4.71:1
|
|
38
|
-
'#dc2626', // 红 4.83:1
|
|
39
|
-
'#0e7490', // 青 5.36:1
|
|
40
|
-
'#7c3aed', // 靛 5.70:1
|
|
41
|
-
'#9a4a08', // 深橙 6.26:1
|
|
42
|
-
];
|
|
43
|
-
/**
|
|
44
|
-
* 由用户 id 稳定映射到调色板颜色。
|
|
45
|
-
*
|
|
46
|
-
* 用 FNV-1a 哈希而非 simple sum:简单的字符求和会让 `user1`/`user2`
|
|
47
|
-
* 这类相邻 id 落到同一颜色,而实际用户 id 常是顺序生成的。
|
|
48
|
-
*/
|
|
49
|
-
function colorForUser(userId) {
|
|
50
|
-
return exports.PRESENCE_PALETTE[hashIndex(userId, exports.PRESENCE_PALETTE.length)];
|
|
51
|
-
}
|
|
52
|
-
/** FNV-1a 32 位哈希 → 取模。 */
|
|
53
|
-
function hashIndex(input, buckets) {
|
|
54
|
-
let h = 0x811c9dc5;
|
|
55
|
-
for (let i = 0; i < input.length; i++) {
|
|
56
|
-
h ^= input.charCodeAt(i);
|
|
57
|
-
// h *= 16777619(用移位避免大整数精度问题)
|
|
58
|
-
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
59
|
-
}
|
|
60
|
-
return buckets > 0 ? h % buckets : 0;
|
|
61
|
-
}
|
|
62
|
-
/** 名字首字(用于头像占位)。中文取首字,英文取首字母大写。 */
|
|
63
|
-
function initialOf(displayName) {
|
|
64
|
-
const t = displayName.trim();
|
|
65
|
-
if (t === '')
|
|
66
|
-
return '?';
|
|
67
|
-
return t.slice(0, 1).toUpperCase();
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 汇总在线用户:去重(同一用户多标签页只显示一次)、排除自己、稳定排序。
|
|
71
|
-
*
|
|
72
|
-
* 稳定性很重要:若按 Map 遍历顺序输出,光标/头像列表会随消息到达顺序抖动。
|
|
73
|
-
* 这里按 displayName 排序,保证界面上顺序稳定。
|
|
74
|
-
*/
|
|
75
|
-
function collectPresence(states, selfUserId) {
|
|
76
|
-
const byUser = new Map();
|
|
77
|
-
for (const s of states) {
|
|
78
|
-
if (!s.userId)
|
|
79
|
-
continue;
|
|
80
|
-
if (selfUserId && s.userId === selfUserId)
|
|
81
|
-
continue;
|
|
82
|
-
if (!byUser.has(s.userId)) {
|
|
83
|
-
byUser.set(s.userId, {
|
|
84
|
-
userId: s.userId,
|
|
85
|
-
displayName: s.displayName || '匿名',
|
|
86
|
-
color: colorForUser(s.userId),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return [...byUser.values()].sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 相对亮度(WCAG),用于决定光标/气泡上该用白字还是深字。
|
|
94
|
-
* 返回 0(黑)~1(白)。
|
|
95
|
-
*/
|
|
96
|
-
function relativeLuminance(hex) {
|
|
97
|
-
const rgb = parseHex(hex);
|
|
98
|
-
if (!rgb)
|
|
99
|
-
return 0;
|
|
100
|
-
const [r, g, b] = rgb.map((v) => {
|
|
101
|
-
const c = v / 255;
|
|
102
|
-
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
103
|
-
});
|
|
104
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
105
|
-
}
|
|
106
|
-
/** 前景深色的基准(与 :root 的 --color-text 接近)。 */
|
|
107
|
-
exports.DARK_TEXT = '#1f2937';
|
|
108
|
-
/** WCAG 对比度。 */
|
|
109
|
-
function contrastRatio(a, b) {
|
|
110
|
-
const la = relativeLuminance(a);
|
|
111
|
-
const lb = relativeLuminance(b);
|
|
112
|
-
const hi = Math.max(la, lb);
|
|
113
|
-
const lo = Math.min(la, lb);
|
|
114
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* 该背景色上应使用的前景色。
|
|
118
|
-
*
|
|
119
|
-
* 实现为「比较白字与深字各自的对比度,取更高的那个」,而不是用一个亮度阈值判断——
|
|
120
|
-
* 阈值是脆弱的:初版用 0.179 作分界,`#2f6feb`(亮度 0.180)被判为用深字,
|
|
121
|
-
* 但实际白字对比度 4.57 优于深字 3.21,选错了。(由单测发现。)
|
|
122
|
-
*/
|
|
123
|
-
function readableTextColor(hex) {
|
|
124
|
-
const withWhite = contrastRatio(hex, '#ffffff');
|
|
125
|
-
const withDark = contrastRatio(hex, exports.DARK_TEXT);
|
|
126
|
-
return withWhite >= withDark ? '#ffffff' : exports.DARK_TEXT;
|
|
127
|
-
}
|
|
128
|
-
/** 解析 #rgb / #rrggbb;非法返回 null。 */
|
|
129
|
-
function parseHex(hex) {
|
|
130
|
-
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
131
|
-
if (!m)
|
|
132
|
-
return null;
|
|
133
|
-
let s = m[1];
|
|
134
|
-
if (s.length === 3)
|
|
135
|
-
s = s.split('').map((c) => c + c).join('');
|
|
136
|
-
return [
|
|
137
|
-
parseInt(s.slice(0, 2), 16),
|
|
138
|
-
parseInt(s.slice(2, 4), 16),
|
|
139
|
-
parseInt(s.slice(4, 6), 16),
|
|
140
|
-
];
|
|
141
|
-
}
|
|
1
|
+
"use strict";function colorForUser(e){return exports.PRESENCE_PALETTE[hashIndex(e,exports.PRESENCE_PALETTE.length)]}function hashIndex(e,r){let t=2166136261;for(let r=0;r<e.length;r++)t^=e.charCodeAt(r),t=t+((t<<1)+(t<<4)+(t<<7)+(t<<8)+(t<<24))>>>0;return r>0?t%r:0}function relativeLuminance(e){const r=parseHex(e);if(!r)return 0;const[t,o,s]=r.map(e=>{const r=e/255;return r<=.03928?r/12.92:((r+.055)/1.055)**2.4});return.2126*t+.7152*o+.0722*s}function contrastRatio(e,r){const t=relativeLuminance(e),o=relativeLuminance(r);return(Math.max(t,o)+.05)/(Math.min(t,o)+.05)}function parseHex(e){const r=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e.trim());if(!r)return null;let t=r[1];return 3===t.length&&(t=t.split("").map(e=>e+e).join("")),[parseInt(t.slice(0,2),16),parseInt(t.slice(2,4),16),parseInt(t.slice(4,6),16)]}Object.defineProperty(exports,"__esModule",{value:!0}),exports.DARK_TEXT=exports.PRESENCE_PALETTE=void 0,exports.colorForUser=colorForUser,exports.hashIndex=hashIndex,exports.initialOf=function initialOf(e){const r=e.trim();return""===r?"?":r.slice(0,1).toUpperCase()},exports.collectPresence=function collectPresence(e,r){const t=new Map;for(const o of e)o.userId&&(r&&o.userId===r||t.has(o.userId)||t.set(o.userId,{userId:o.userId,displayName:o.displayName||"匿名",color:colorForUser(o.userId)}));return[...t.values()].sort((e,r)=>e.displayName.localeCompare(r.displayName,"zh-Hans-CN"))},exports.relativeLuminance=relativeLuminance,exports.contrastRatio=contrastRatio,exports.readableTextColor=function readableTextColor(e){return contrastRatio(e,"#ffffff")>=contrastRatio(e,exports.DARK_TEXT)?"#ffffff":exports.DARK_TEXT},exports.parseHex=parseHex,exports.PRESENCE_PALETTE=["#2f6feb","#127a45","#b45309","#c026d3","#dc2626","#0e7490","#7c3aed","#9a4a08"],exports.DARK_TEXT="#1f2937";
|
|
@@ -1,36 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isSafeHref = isSafeHref;
|
|
4
|
-
exports.safeHref = safeHref;
|
|
5
|
-
/**
|
|
6
|
-
* href 协议白名单(客户端纵深防御,L4)。
|
|
7
|
-
*
|
|
8
|
-
* 服务端在写入与读取投影时都做了协议白名单(挡 `javascript:`),但历史数据、
|
|
9
|
-
* 导入内容或服务端回归仍可能让危险 href 出现在正文 JSON 里。渲染器直接把它
|
|
10
|
-
* 写进 `<a href>`,等于给浏览器一个可执行入口——因此在**渲染侧**再兜一层:
|
|
11
|
-
* 只放行确定安全的协议;其余(含 `javascript:` / `data:` / `vbscript:` 等)
|
|
12
|
-
* 一律降级为不可点击的锚点(href 置为 `#`)。
|
|
13
|
-
*
|
|
14
|
-
* 允许:http(s)、mailto、以 `/` 或 `#` 开头的站内相对链接,以及相对路径。
|
|
15
|
-
*/
|
|
16
|
-
const SAFE_ABSOLUTE = /^(https?:|mailto:)/i;
|
|
17
|
-
function isSafeHref(raw) {
|
|
18
|
-
if (typeof raw !== 'string')
|
|
19
|
-
return false;
|
|
20
|
-
// 去掉控制字符与空白后再判断,避免 `java\nscript:` 之类绕过
|
|
21
|
-
const href = raw.replace(/[\u0000-\u001f\u007f\s]/g, '').toLowerCase();
|
|
22
|
-
if (href === '')
|
|
23
|
-
return false;
|
|
24
|
-
if (href.startsWith('/') || href.startsWith('#'))
|
|
25
|
-
return true;
|
|
26
|
-
if (SAFE_ABSOLUTE.test(href))
|
|
27
|
-
return true;
|
|
28
|
-
// 相对路径(无协议、无 `:` 前缀):允许。含 `:` 的一律按协议处理并要求在白名单内。
|
|
29
|
-
if (!href.includes(':'))
|
|
30
|
-
return true;
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
/** 渲染用:安全则原样返回,否则返回 '#'。 */
|
|
34
|
-
function safeHref(raw) {
|
|
35
|
-
return isSafeHref(raw) ? String(raw) : '#';
|
|
36
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isSafeHref=isSafeHref,exports.safeHref=function safeHref(e){return isSafeHref(e)?String(e):"#"};const e=/^(https?:|mailto:)/i;function isSafeHref(t){if("string"!=typeof t)return!1;const r=t.replace(/[\u0000-\u001f\u007f\s]/g,"").toLowerCase();return!(""===r||!r.startsWith("/")&&!r.startsWith("#")&&!e.test(r)&&r.includes(":"))}
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CELL_BACKGROUNDS = void 0;
|
|
4
|
-
exports.cellBackground = cellBackground;
|
|
5
|
-
/** 固定单元格底色,不接受任意 CSS;读写端共用,服务端同步白名单。 */
|
|
6
|
-
exports.CELL_BACKGROUNDS = [
|
|
7
|
-
{ name: '默认', value: null },
|
|
8
|
-
{ name: '灰', value: '#f1f3f5' },
|
|
9
|
-
{ name: '黄', value: '#fff3bf' },
|
|
10
|
-
{ name: '绿', value: '#d3f9d8' },
|
|
11
|
-
{ name: '蓝', value: '#d0ebff' },
|
|
12
|
-
{ name: '紫', value: '#e5dbff' },
|
|
13
|
-
{ name: '红', value: '#ffe3e3' },
|
|
14
|
-
];
|
|
15
|
-
function cellBackground(value) {
|
|
16
|
-
return typeof value === 'string' && exports.CELL_BACKGROUNDS.some((c) => c.value === value)
|
|
17
|
-
? value : null;
|
|
18
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CELL_BACKGROUNDS=void 0,exports.cellBackground=function cellBackground(e){return"string"==typeof e&&exports.CELL_BACKGROUNDS.some(a=>a.value===e)?e:null},exports.CELL_BACKGROUNDS=[{name:"默认",value:null},{name:"灰",value:"#f1f3f5"},{name:"黄",value:"#fff3bf"},{name:"绿",value:"#d3f9d8"},{name:"蓝",value:"#d0ebff"},{name:"紫",value:"#e5dbff"},{name:"红",value:"#ffe3e3"}];
|
|
@@ -1,28 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TABLE_CELL_MIN_WIDTH = void 0;
|
|
4
|
-
exports.tableColumnWidths = tableColumnWidths;
|
|
5
|
-
exports.tableMinWidth = tableMinWidth;
|
|
6
|
-
/** 自动列宽保留可读空间;窄屏在表格内部滚动,不把长文本压成单字列。 */
|
|
7
|
-
exports.TABLE_CELL_MIN_WIDTH = 120;
|
|
8
|
-
/** 首行合并单元格的 colwidth 数组包含多个视觉列,必须逐列展开。 */
|
|
9
|
-
function tableColumnWidths(table) {
|
|
10
|
-
const row = table.content?.find((node) => node.type === 'tableRow');
|
|
11
|
-
const widths = [];
|
|
12
|
-
for (const cell of row?.content ?? []) {
|
|
13
|
-
const span = Number(cell.attrs?.colspan ?? 1);
|
|
14
|
-
const count = Number.isSafeInteger(span) && span > 0 ? Math.min(span, 1000) : 1;
|
|
15
|
-
const colwidth = cell.attrs?.colwidth;
|
|
16
|
-
for (let index = 0; index < count; index += 1) {
|
|
17
|
-
const value = Array.isArray(colwidth) ? colwidth[index] : undefined;
|
|
18
|
-
widths.push(typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return widths;
|
|
22
|
-
}
|
|
23
|
-
/** 未设置宽度的列与编辑器 cellMinWidth 共用同一默认值;保留历史显式列宽。 */
|
|
24
|
-
function tableMinWidth(widths) {
|
|
25
|
-
return widths.length > 0
|
|
26
|
-
? widths.reduce((sum, width) => sum + (width > 0 ? width : exports.TABLE_CELL_MIN_WIDTH), 0)
|
|
27
|
-
: undefined;
|
|
28
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.TABLE_CELL_MIN_WIDTH=void 0,exports.tableColumnWidths=function tableColumnWidths(t){const e=t.content?.find(t=>"tableRow"===t.type),o=[];for(const t of e?.content??[]){const e=Number(t.attrs?.colspan??1),r=Number.isSafeInteger(e)&&e>0?Math.min(e,1e3):1,n=t.attrs?.colwidth;for(let t=0;t<r;t+=1){const e=Array.isArray(n)?n[t]:void 0;o.push("number"==typeof e&&Number.isFinite(e)&&e>0?e:0)}}return o},exports.tableMinWidth=function tableMinWidth(t){return t.length>0?t.reduce((t,e)=>t+(e>0?e:exports.TABLE_CELL_MIN_WIDTH),0):void 0},exports.TABLE_CELL_MIN_WIDTH=120;
|
package/dist/cjs/shared/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});
|
|
@@ -1,102 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeVideoEmbed = normalizeVideoEmbed;
|
|
4
|
-
exports.parseVideoUrl = parseVideoUrl;
|
|
5
|
-
exports.videoSourceUrl = videoSourceUrl;
|
|
6
|
-
exports.videoPlayerUrl = videoPlayerUrl;
|
|
7
|
-
function normalizeVideoEmbed(raw) {
|
|
8
|
-
if (!raw || typeof raw !== 'object')
|
|
9
|
-
return null;
|
|
10
|
-
const data = raw;
|
|
11
|
-
const validId = data.provider === 'youtube'
|
|
12
|
-
? typeof data.videoId === 'string' && /^[\w-]{11}$/.test(data.videoId)
|
|
13
|
-
: data.provider === 'bilibili' && typeof data.videoId === 'string' &&
|
|
14
|
-
/^(BV[A-Za-z0-9]{10}|av[1-9]\d{0,15})$/.test(data.videoId);
|
|
15
|
-
const page = data.page ?? 1;
|
|
16
|
-
const startSeconds = data.startSeconds ?? 0;
|
|
17
|
-
if (!validId || !Number.isInteger(page) || Number(page) < 1 || Number(page) > 1000 ||
|
|
18
|
-
!Number.isInteger(startSeconds) || Number(startSeconds) < 0 || Number(startSeconds) > 604800)
|
|
19
|
-
return null;
|
|
20
|
-
return { provider: data.provider, videoId: data.videoId,
|
|
21
|
-
page: data.provider === 'youtube' ? 1 : Number(page), startSeconds: Number(startSeconds) };
|
|
22
|
-
}
|
|
23
|
-
function seconds(value) {
|
|
24
|
-
if (!value)
|
|
25
|
-
return 0;
|
|
26
|
-
if (/^\d+$/.test(value))
|
|
27
|
-
return Number(value);
|
|
28
|
-
const match = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(value);
|
|
29
|
-
if (!match || !match[0])
|
|
30
|
-
return NaN;
|
|
31
|
-
return Number(match[1] ?? 0) * 3600 + Number(match[2] ?? 0) * 60 + Number(match[3] ?? 0);
|
|
32
|
-
}
|
|
33
|
-
function parseVideoUrl(value) {
|
|
34
|
-
let url;
|
|
35
|
-
try {
|
|
36
|
-
url = new URL(value.trim());
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.port)
|
|
42
|
-
return null;
|
|
43
|
-
const host = url.hostname.toLowerCase();
|
|
44
|
-
const path = url.pathname.split('/').filter(Boolean);
|
|
45
|
-
let provider;
|
|
46
|
-
let videoId;
|
|
47
|
-
if (host === 'youtu.be') {
|
|
48
|
-
provider = 'youtube';
|
|
49
|
-
videoId = path.length === 1 ? path[0] : null;
|
|
50
|
-
}
|
|
51
|
-
else if (['youtube.com', 'www.youtube.com', 'm.youtube.com', 'www.youtube-nocookie.com'].includes(host)) {
|
|
52
|
-
provider = 'youtube';
|
|
53
|
-
videoId = url.pathname === '/watch' ? url.searchParams.get('v')
|
|
54
|
-
: ['embed', 'shorts', 'live'].includes(path[0]) && path.length === 2 ? path[1] : null;
|
|
55
|
-
}
|
|
56
|
-
else if (['bilibili.com', 'www.bilibili.com', 'm.bilibili.com'].includes(host)) {
|
|
57
|
-
provider = 'bilibili';
|
|
58
|
-
videoId = path[0] === 'video' && path.length === 2 ? path[1] : null;
|
|
59
|
-
}
|
|
60
|
-
else if (host === 'player.bilibili.com' && url.pathname === '/player.html') {
|
|
61
|
-
provider = 'bilibili';
|
|
62
|
-
videoId = url.searchParams.get('bvid') ?? (url.searchParams.get('aid') ? `av${url.searchParams.get('aid')}` : null);
|
|
63
|
-
}
|
|
64
|
-
else
|
|
65
|
-
return null;
|
|
66
|
-
const hashTime = /^#t=(.*)$/.exec(url.hash)?.[1] ?? null;
|
|
67
|
-
return normalizeVideoEmbed({ provider, videoId, page: Number(url.searchParams.get('p') ?? 1),
|
|
68
|
-
startSeconds: seconds(url.searchParams.get('start') ?? url.searchParams.get('t') ?? hashTime) });
|
|
69
|
-
}
|
|
70
|
-
function videoSourceUrl(data) {
|
|
71
|
-
const safe = normalizeVideoEmbed(data);
|
|
72
|
-
if (!safe)
|
|
73
|
-
return '';
|
|
74
|
-
const url = new URL(safe.provider === 'youtube'
|
|
75
|
-
? `https://www.youtube.com/watch?v=${safe.videoId}`
|
|
76
|
-
: `https://www.bilibili.com/video/${safe.videoId}`);
|
|
77
|
-
if (safe.provider === 'bilibili' && safe.page > 1)
|
|
78
|
-
url.searchParams.set('p', String(safe.page));
|
|
79
|
-
if (safe.startSeconds)
|
|
80
|
-
url.searchParams.set('t', String(safe.startSeconds));
|
|
81
|
-
return url.href;
|
|
82
|
-
}
|
|
83
|
-
function videoPlayerUrl(data) {
|
|
84
|
-
const safe = normalizeVideoEmbed(data);
|
|
85
|
-
if (!safe)
|
|
86
|
-
return '';
|
|
87
|
-
const url = new URL(safe.provider === 'youtube'
|
|
88
|
-
? `https://www.youtube.com/embed/${safe.videoId}` : 'https://player.bilibili.com/player.html');
|
|
89
|
-
url.searchParams.set('autoplay', '0');
|
|
90
|
-
if (safe.provider === 'youtube') {
|
|
91
|
-
url.searchParams.set('playsinline', '1');
|
|
92
|
-
if (safe.startSeconds)
|
|
93
|
-
url.searchParams.set('start', String(safe.startSeconds));
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
url.searchParams.set(safe.videoId.startsWith('BV') ? 'bvid' : 'aid', safe.videoId.startsWith('BV') ? safe.videoId : safe.videoId.slice(2));
|
|
97
|
-
url.searchParams.set('p', String(safe.page));
|
|
98
|
-
url.searchParams.set('t', String(safe.startSeconds));
|
|
99
|
-
url.searchParams.set('danmaku', '0');
|
|
100
|
-
}
|
|
101
|
-
return url.href;
|
|
102
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.VIDEO_PROVIDER_LABELS=void 0,exports.normalizeVideoEmbed=normalizeVideoEmbed,exports.inspectVideoUrl=inspectVideoUrl,exports.parseVideoUrl=function parseVideoUrl(e){return inspectVideoUrl(e).data},exports.videoSourceUrl=function videoSourceUrl(e){const t=normalizeVideoEmbed(e);if(!t)return"";const{provider:r,videoId:i}=t,s=new URL("youtube"===r?`https://www.youtube.com/watch?v=${i}`:"bilibili"===r?i.startsWith("ep")||i.startsWith("ss")?`https://www.bilibili.com/bangumi/play/${i}`:`https://www.bilibili.com/video/${i}`:"vimeo"===r?`https://vimeo.com/${i}`:"youku"===r?`https://v.youku.com/v_show/id_${i}.html`:"tencent"===r?`https://v.qq.com/x/page/${i}.html`:`https://www.acfun.cn/v/${i}`);return"bilibili"===r&&t.page>1&&s.searchParams.set("p",String(t.page)),t.startSeconds&&"vimeo"!==r&&s.searchParams.set("t",String(t.startSeconds)),s.href},exports.videoPlayerUrl=function videoPlayerUrl(e){const t=normalizeVideoEmbed(e);if(!t)return"";const{provider:r,videoId:i}=t;if("youtube"===r){const e=new URL(`https://www.youtube.com/embed/${i}`);return e.searchParams.set("autoplay","0"),e.searchParams.set("playsinline","1"),t.startSeconds&&e.searchParams.set("start",String(t.startSeconds)),e.href}if("vimeo"===r){const e=new URL(`https://player.vimeo.com/video/${i}`);return e.searchParams.set("autoplay","0"),e.href}if("youku"===r){const e=new URL(`https://player.youku.com/embed/${i}`);return e.searchParams.set("autoplay","0"),e.href}if("tencent"===r){const e=new URL("https://v.qq.com/txp/iframe/player.html");return e.searchParams.set("vid",i),e.searchParams.set("autoplay","false"),e.href}if("acfun"===r)return`https://www.acfun.cn/player/${i}`;const s=new URL("https://player.bilibili.com/player.html");return s.searchParams.set("autoplay","0"),i.startsWith("ep")||i.startsWith("ss")?s.searchParams.set(i.startsWith("ep")?"episodeId":"seasonId",i.slice(2)):(s.searchParams.set(i.startsWith("BV")?"bvid":"aid",i.startsWith("BV")?i:i.slice(2)),s.searchParams.set("p",String(t.page)),s.searchParams.set("t",String(t.startSeconds)),s.searchParams.set("danmaku","0")),s.href},exports.VIDEO_PROVIDER_LABELS={youtube:"YouTube",bilibili:"bilibili",vimeo:"Vimeo",youku:"优酷",tencent:"腾讯视频",acfun:"AcFun"};const e={youtube:/^[\w-]{11}$/,bilibili:/^(BV[A-Za-z0-9]{10}|av[1-9]\d{0,15}|ep\d{1,12}|ss\d{1,12})$/,vimeo:/^[1-9]\d{0,11}$/,youku:/^[A-Za-z0-9=_+-]{6,44}$/,tencent:/^[A-Za-z0-9]{8,24}$/,acfun:/^ac\d{1,12}$/};function normalizeVideoEmbed(t){if(!t||"object"!=typeof t)return null;const r=t,i=function isProvider(t){return"string"==typeof t&&Object.hasOwn(e,t)}(r.provider)&&"string"==typeof r.videoId&&e[r.provider].test(r.videoId),s=r.page??1,a=r.startSeconds??0;return!i||!Number.isInteger(s)||Number(s)<1||Number(s)>1e3||!Number.isInteger(a)||Number(a)<0||Number(a)>604800?null:{provider:r.provider,videoId:r.videoId,page:"bilibili"===r.provider?Number(s):1,startSeconds:Number(a)}}const t=["bilibili.com","www.bilibili.com","m.bilibili.com","bangumi.bilibili.com"],r=/^(BV[A-Za-z0-9]{10}|av[1-9]\d{0,15}|ep\d{1,12}|ss\d{1,12})$/,i=["youtube.com","www.youtube.com","m.youtube.com","www.youtube-nocookie.com","youtube-nocookie.com"];function seconds(e){if(!e)return 0;if(/^\d+$/.test(e))return Number(e);const t=/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(e);return t&&t[0]?3600*Number(t[1]??0)+60*Number(t[2]??0)+Number(t[3]??0):NaN}function inspectVideoUrl(e){const s=e.trim();if(!s)return{data:null,issue:"empty"};let a;try{a=new URL(s)}catch{return{data:null,issue:"invalid"}}if(!["https:","http:"].includes(a.protocol)||a.username||a.password||a.port)return{data:null,issue:"invalid"};const o=a.hostname.toLowerCase(),n=a.pathname.split("/").filter(Boolean);let u,c;if("youtu.be"===o||i.includes(o))u="youtube",c=function parseYouTube(e,t,r){return"youtu.be"===e?1===r.length?r[0]:null:"/watch"===t.pathname?t.searchParams.get("v"):["embed","shorts","live","v"].includes(r[0])&&2===r.length?r[1]:null}(o,a,n);else if(t.includes(o)||"player.bilibili.com"===o)u="bilibili",c=function parseBilibili(e,t,i){return"player.bilibili.com"===e?"/player.html"!==t.pathname?null:t.searchParams.get("bvid")??(t.searchParams.get("aid")?`av${t.searchParams.get("aid")}`:null):i.find(e=>r.test(e))??null}(o,a,n);else{if("b23.tv"===o)return{data:null,issue:"short-link"};if("vimeo.com"===o||"www.vimeo.com"===o||"player.vimeo.com"===o)u="vimeo",c=function parseVimeo(e,t){if("player.vimeo.com"===e)return"video"===t[0]?t[1]??null:null;const r=t.at(-1)??"";return/^\d+$/.test(r)?r:null}(o,n);else if("v.youku.com"===o||"m.youku.com"===o||"player.youku.com"===o)u="youku",c=function parseYouku(e,t,r){if("player.youku.com"===e)return"embed"===r[0]?r[1]:null;const i=r.find(e=>/^id_.+\.html$/.test(e));return i?i.slice(3,-5):t.searchParams.get("vid")}(o,a,n);else if(["v.qq.com","m.v.qq.com","www.v.qq.com"].includes(o))u="tencent",c=function parseTencent(e,t,r){const i=t.searchParams.get("vid")??t.searchParams.get("video_id");if(i)return i;const s=(r.at(-1)??"").replace(/\.html?$/,"");return/^[A-Za-z0-9]{8,24}$/.test(s)?s:null}(0,a,n);else{if("acfun.cn"!==o&&"www.acfun.cn"!==o&&"m.acfun.cn"!==o)return{data:null,issue:"unsupported"};u="acfun",c=function parseAcfun(e,t){const r=e.find(e=>/^ac\d{1,12}$/.test(e));if(r)return r;const i=t.searchParams.get("ac");if(i&&/^\d{1,12}$/.test(i))return`ac${i}`;const s=e.join("/").match(/(^|[/_])ac(\d{1,12})/)?.[2];return s?`ac${s}`:null}(n,a)}}if(!c)return{data:null,issue:"missing-id"};const l=/^#t=(.*)$/.exec(a.hash)?.[1]??null,m=normalizeVideoEmbed({provider:u,videoId:c,page:Number(a.searchParams.get("p")??1),startSeconds:seconds(a.searchParams.get("start")??a.searchParams.get("t")??l)});return{data:m,issue:m?null:"invalid"}}
|