mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
feat(core): moving in affine-reader doc parsers (#12840)
fix AI-191 #### PR Dependency Tree * **PR #12840** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced the ability to convert rich text documents into Markdown, supporting a wide range of content types such as headings, lists, tables, images, code blocks, attachments, and embedded documents. - Added support for parsing collaborative document structures and rendering them as structured Markdown or parsed representations. - Enhanced handling of database and table blocks, including conversion to Markdown tables with headers and cell content. - **Documentation** - Added a README noting the use of a forked Markdown converter. - **Tests** - Added new test coverage for document parsing features. <!-- end of auto-generated comment: release notes by coderabbit.ai --> #### PR Dependency Tree * **PR #12840** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
A fork of https://github.com/frysztak/quill-delta-to-markdown
|
||||
@@ -0,0 +1,95 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
import { Node } from './utils/node';
|
||||
import { encodeLink } from './utils/url';
|
||||
|
||||
export interface InlineReference {
|
||||
type: 'LinkedPage';
|
||||
pageId: string;
|
||||
title?: string;
|
||||
params?: { mode: 'doc' | 'edgeless' };
|
||||
}
|
||||
|
||||
export interface ConverterOptions {
|
||||
convertInlineReferenceLink?: (reference: InlineReference) => {
|
||||
title: string;
|
||||
link: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConvertInlineReferenceLink = (reference: InlineReference) => {
|
||||
return {
|
||||
title: reference.title || '',
|
||||
link: [reference.type, reference.pageId, reference.params?.mode]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
};
|
||||
};
|
||||
|
||||
export function getConverters(opts: ConverterOptions = {}) {
|
||||
const { convertInlineReferenceLink = defaultConvertInlineReferenceLink } =
|
||||
opts;
|
||||
|
||||
return {
|
||||
embed: {
|
||||
image: function (src) {
|
||||
this.append(' + ')');
|
||||
},
|
||||
// Not a default Quill feature, converts custom divider embed blot added when
|
||||
// creating quill editor instance.
|
||||
// See https://quilljs.com/guides/cloning-medium-with-parchment/#dividers
|
||||
thematic_break: function () {
|
||||
this.open = '\n---\n' + this.open;
|
||||
},
|
||||
},
|
||||
|
||||
inline: {
|
||||
italic: function () {
|
||||
return ['_', '_'];
|
||||
},
|
||||
bold: function () {
|
||||
return ['**', '**'];
|
||||
},
|
||||
link: function (url) {
|
||||
return ['[', '](' + url + ')'];
|
||||
},
|
||||
reference: function (reference: InlineReference) {
|
||||
const { title, link } = convertInlineReferenceLink(reference);
|
||||
return ['[', `${title}](${link})`];
|
||||
},
|
||||
strike: function () {
|
||||
return ['~~', '~~'];
|
||||
},
|
||||
code: function () {
|
||||
return ['`', '`'];
|
||||
},
|
||||
},
|
||||
|
||||
block: {
|
||||
header: function ({ header }) {
|
||||
this.open = '#'.repeat(header) + ' ' + this.open;
|
||||
},
|
||||
blockquote: function () {
|
||||
this.open = '> ' + this.open;
|
||||
},
|
||||
list: {
|
||||
group: function () {
|
||||
return new Node(['', '\n']);
|
||||
},
|
||||
line: function (attrs, group) {
|
||||
if (attrs.list === 'bullet') {
|
||||
this.open = '- ' + this.open;
|
||||
} else if (attrs.list === 'checked') {
|
||||
this.open = '- [x] ' + this.open;
|
||||
} else if (attrs.list === 'unchecked') {
|
||||
this.open = '- [ ] ' + this.open;
|
||||
} else if (attrs.list === 'ordered') {
|
||||
group.count = group.count || 0;
|
||||
var count = ++group.count;
|
||||
this.open = count + '. ' + this.open;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
import { Node } from './utils/node';
|
||||
|
||||
export const deltaToMd = (delta, converters) => {
|
||||
return convert(delta, converters).render().trimEnd() + '\n';
|
||||
};
|
||||
|
||||
function convert(ops, converters) {
|
||||
let group, line, el, activeInline, beginningOfLine;
|
||||
let root = new Node();
|
||||
|
||||
function newLine() {
|
||||
el = line = new Node(['', '\n']);
|
||||
root.append(line);
|
||||
activeInline = {};
|
||||
}
|
||||
newLine();
|
||||
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i];
|
||||
|
||||
if (typeof op.insert === 'object') {
|
||||
for (let k in op.insert) {
|
||||
if (converters.embed[k]) {
|
||||
applyInlineAttributes(op.attributes);
|
||||
converters.embed[k].call(el, op.insert[k], op.attributes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let lines = op.insert.split('\n');
|
||||
|
||||
if (hasBlockLevelAttribute(op.attributes, converters)) {
|
||||
// Some line-level styling (ie headings) is applied by inserting a \n
|
||||
// with the style; the style applies back to the previous \n.
|
||||
// There *should* only be one style in an insert operation.
|
||||
|
||||
for (let j = 1; j < lines.length; j++) {
|
||||
for (let attr in op.attributes) {
|
||||
if (converters.block[attr]) {
|
||||
let fn = converters.block[attr];
|
||||
if (typeof fn === 'object') {
|
||||
if (group && group.type !== attr) {
|
||||
group = null;
|
||||
}
|
||||
if (!group && fn.group) {
|
||||
group = {
|
||||
el: fn.group(),
|
||||
type: attr,
|
||||
value: op.attributes[attr],
|
||||
distance: 0,
|
||||
};
|
||||
root.append(group.el);
|
||||
}
|
||||
|
||||
if (group) {
|
||||
group.el.append(line);
|
||||
group.distance = 0;
|
||||
}
|
||||
fn = fn.line;
|
||||
}
|
||||
|
||||
fn.call(line, op.attributes, group);
|
||||
newLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
beginningOfLine = true;
|
||||
} else {
|
||||
for (let l = 0; l < lines.length; l++) {
|
||||
if ((l > 0 || beginningOfLine) && group && ++group.distance >= 2) {
|
||||
group = null;
|
||||
}
|
||||
applyInlineAttributes(
|
||||
op.attributes,
|
||||
ops[i + 1] && ops[i + 1].attributes
|
||||
);
|
||||
el.append(lines[l]);
|
||||
if (l < lines.length - 1) {
|
||||
newLine();
|
||||
}
|
||||
}
|
||||
beginningOfLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
|
||||
function applyInlineAttributes(attrs, next?: any) {
|
||||
let first: any[] = [];
|
||||
let then: any[] = [];
|
||||
attrs = attrs || {};
|
||||
|
||||
let tag = el,
|
||||
seen = {};
|
||||
while (tag._format) {
|
||||
seen[tag._format] = true;
|
||||
if (!attrs[tag._format] || tag.open !== tag.close) {
|
||||
for (let k in seen) {
|
||||
delete activeInline[k];
|
||||
}
|
||||
el = tag.parent();
|
||||
}
|
||||
|
||||
tag = tag.parent();
|
||||
}
|
||||
|
||||
for (let attr in attrs) {
|
||||
if (converters.inline[attr] && attrs[attr]) {
|
||||
if (activeInline[attr] && activeInline[attr] === attrs[attr]) {
|
||||
continue; // do nothing -- we should already be inside this style's tag
|
||||
}
|
||||
|
||||
if (next && attrs[attr] === next[attr]) {
|
||||
first.push(attr); // if the next operation has the same style, this should be the outermost tag
|
||||
} else {
|
||||
then.push(attr);
|
||||
}
|
||||
activeInline[attr] = attrs[attr];
|
||||
}
|
||||
}
|
||||
|
||||
first.forEach(apply);
|
||||
then.forEach(apply);
|
||||
|
||||
function apply(fmt) {
|
||||
let newEl = converters.inline[fmt].call(null, attrs[fmt]);
|
||||
if (Array.isArray(newEl)) {
|
||||
newEl = new Node(newEl);
|
||||
}
|
||||
newEl._format = fmt;
|
||||
el.append(newEl);
|
||||
el = newEl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasBlockLevelAttribute(attrs, converters) {
|
||||
for (let k in attrs) {
|
||||
if (Object.keys(converters.block).includes(k)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { getConverters } from './delta-converters';
|
||||
export { deltaToMd } from './delta-to-md';
|
||||
@@ -0,0 +1,66 @@
|
||||
// eslint-disable
|
||||
// @ts-nocheck
|
||||
let id = 0;
|
||||
|
||||
export class Node {
|
||||
id = ++id;
|
||||
children: Node[];
|
||||
open: string;
|
||||
close: string;
|
||||
text: string;
|
||||
|
||||
_format: string;
|
||||
_parent: Node;
|
||||
|
||||
constructor(data?: string[] | string) {
|
||||
if (Array.isArray(data)) {
|
||||
this.open = data[0];
|
||||
this.close = data[1];
|
||||
} else if (typeof data === 'string') {
|
||||
this.text = data;
|
||||
}
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
append(e: Node) {
|
||||
if (!(e instanceof Node)) {
|
||||
e = new Node(e);
|
||||
}
|
||||
if (e._parent) {
|
||||
const idx = e._parent.children.indexOf(e);
|
||||
e._parent.children.splice(idx, 1);
|
||||
}
|
||||
e._parent = this;
|
||||
this.children = this.children.concat(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inner =
|
||||
(this.text || '') + this.children.map(c => c.render()).join('');
|
||||
|
||||
if (
|
||||
inner.trim() === '' &&
|
||||
this.open === this.close &&
|
||||
this.open &&
|
||||
this.close
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const wrapped = this.open && this.close;
|
||||
const emptyInner = inner.trim() === '';
|
||||
const fragments = [
|
||||
inner.startsWith(' ') && !emptyInner && wrapped ? ' ' : '',
|
||||
this.open,
|
||||
wrapped ? inner.trim() : inner,
|
||||
this.close,
|
||||
inner.endsWith(' ') && !emptyInner && wrapped ? ' ' : '',
|
||||
].filter(f => f);
|
||||
|
||||
return fragments.join('');
|
||||
}
|
||||
|
||||
parent() {
|
||||
return this._parent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const encodeLink = (link: string) =>
|
||||
encodeURI(link)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/(\?|&)response-content-disposition=attachment.*$/, '');
|
||||
Reference in New Issue
Block a user