mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 18:20:39 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bee2cb0fa | |||
| 1addd17d64 | |||
| 842c39c3be | |||
| 26674b0cb8 | |||
| cafff4e0eb | |||
| abc3f9f23f | |||
| 5dbffba08d | |||
| 2ec7de7e32 | |||
| e5e5c0a8ba | |||
| c644a46b8d | |||
| 7e892b3a7e | |||
| 848145150d | |||
| dee6be11fb | |||
| abda70d2c8 | |||
| 40104f2f87 | |||
| 162b7adc1b | |||
| 6289981fd1 | |||
| 0e581c915c | |||
| 59a791fe1f | |||
| 378bb3795d | |||
| 60b994f38b | |||
| 1b2a4377fd |
@@ -1 +1,2 @@
|
||||
/blocksuite/ @toeverything/blocksuite-core
|
||||
/packages/frontend/core/src/blocksuite @toeverything/blocksuite-core
|
||||
|
||||
Generated
+1285
-42
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,12 @@ affine_common = { path = "./packages/common/native" }
|
||||
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
|
||||
anyhow = "1"
|
||||
base64-simd = "0.8"
|
||||
block2 = "0.6"
|
||||
chrono = "0.4"
|
||||
core-foundation = "0.10"
|
||||
coreaudio-rs = "0.12"
|
||||
criterion2 = { version = "2", default-features = false }
|
||||
dispatch2 = "0.2"
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
@@ -31,6 +35,8 @@ once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.9"
|
||||
rayon = "1.10"
|
||||
rubato = "0.16"
|
||||
screencapturekit = "0.3"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@blocksuite/icons": "^2.2.3",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.3",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
|
||||
import { WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
@@ -45,8 +44,3 @@ export const viewportOverlayWidget = WidgetViewExtension(
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
|
||||
);
|
||||
export const scrollAnchoringWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_SCROLL_ANCHORING_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -29,7 +29,10 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
|
||||
snapshots
|
||||
.filter((snapshot): snapshot is DocSnapshot => !!snapshot)
|
||||
.map(async snapshot => {
|
||||
const snapshotName = `${snapshot.meta.title || 'untitled'}.snapshot.json`;
|
||||
// Use the title and id as the snapshot file name
|
||||
const title = snapshot.meta.title || 'untitled';
|
||||
const id = snapshot.meta.id;
|
||||
const snapshotName = `${title}-${id}.snapshot.json`;
|
||||
await zip.file(snapshotName, JSON.stringify(snapshot, null, 2));
|
||||
})
|
||||
);
|
||||
@@ -63,6 +66,7 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
|
||||
}
|
||||
|
||||
const downloadBlob = await zip.generate();
|
||||
// Use the collection id as the zip file name
|
||||
return download(downloadBlob, `${collection.id}.bs.zip`);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -30,7 +30,10 @@ export const tableBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'table') {
|
||||
const tableProps = parseTableFromHtml(o.node);
|
||||
const astToDelta = context.deltaConverter.astToDelta.bind(
|
||||
context.deltaConverter
|
||||
);
|
||||
const tableProps = parseTableFromHtml(o.node, astToDelta);
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
|
||||
@@ -25,12 +25,15 @@ export const tableBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (o.node.type === 'table') {
|
||||
const astToDelta = context.deltaConverter.astToDelta.bind(
|
||||
context.deltaConverter
|
||||
);
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: TableModelFlavour,
|
||||
props: parseTableFromMarkdown(o.node),
|
||||
props: parseTableFromMarkdown(o.node, astToDelta),
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
import { createTableProps, formatTable, processTable } from './utils.js';
|
||||
@@ -21,10 +22,14 @@ export const tableBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = {
|
||||
const text = o.node.content;
|
||||
const rowTexts = text.split('\n');
|
||||
if (rowTexts.length <= 1) return;
|
||||
const rowTextLists: string[][] = [];
|
||||
const rowTextLists: DeltaInsert[][][] = [];
|
||||
let columnCount: number | null = null;
|
||||
for (const row of rowTexts) {
|
||||
const cells = row.split('\t');
|
||||
const cells = row.split('\t').map<DeltaInsert[]>(text => [
|
||||
{
|
||||
insert: text,
|
||||
},
|
||||
]);
|
||||
if (cells.length <= 1) return;
|
||||
if (columnCount == null) {
|
||||
columnCount = cells.length;
|
||||
|
||||
@@ -5,14 +5,23 @@ import type {
|
||||
TableRow,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
AdapterTextUtils,
|
||||
HastUtils,
|
||||
type HtmlAST,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { HastUtils } from '@blocksuite/affine-shared/adapters';
|
||||
import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { Element, ElementContent } from 'hast';
|
||||
import type { PhrasingContent, Table as MarkdownTable, TableCell } from 'mdast';
|
||||
import type { Element } from 'hast';
|
||||
import type { Table as MarkdownTable } from 'mdast';
|
||||
|
||||
type RichTextType = DeltaInsert[];
|
||||
const createRichText = (text: RichTextType) => {
|
||||
return {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: text,
|
||||
};
|
||||
};
|
||||
function calculateColumnWidths(rows: string[][]): number[] {
|
||||
return (
|
||||
rows[0]?.map((_, colIndex) =>
|
||||
@@ -92,15 +101,6 @@ export const processTable = (
|
||||
});
|
||||
return table;
|
||||
};
|
||||
const getTextFromElement = (element: ElementContent): string => {
|
||||
if (element.type === 'text') {
|
||||
return element.value.trim();
|
||||
}
|
||||
if (element.type === 'element') {
|
||||
return element.children.map(child => getTextFromElement(child)).join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
|
||||
if (!node) {
|
||||
@@ -120,7 +120,7 @@ const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
export const createTableProps = (rowTextLists: string[][]) => {
|
||||
export const createTableProps = (deltasLists: RichTextType[][]) => {
|
||||
const createIdAndOrder = (count: number) => {
|
||||
const result: { id: string; order: string }[] = Array.from({
|
||||
length: count,
|
||||
@@ -135,8 +135,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const columnCount = Math.max(...rowTextLists.map(row => row.length));
|
||||
const rowCount = rowTextLists.length;
|
||||
const columnCount = Math.max(...deltasLists.map(row => row.length));
|
||||
const rowCount = deltasLists.length;
|
||||
|
||||
const columns: TableColumn[] = createIdAndOrder(columnCount).map(v => ({
|
||||
columnId: v.id,
|
||||
@@ -156,9 +156,9 @@ export const createTableProps = (rowTextLists: string[][]) => {
|
||||
continue;
|
||||
}
|
||||
const cellId = `${row.rowId}:${column.columnId}`;
|
||||
const text = rowTextLists[i]?.[j];
|
||||
const text = deltasLists[i]?.[j];
|
||||
cells[cellId] = {
|
||||
text: AdapterTextUtils.createText(text ?? ''),
|
||||
text: createRichText(text ?? []),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
|
||||
};
|
||||
|
||||
export const parseTableFromHtml = (
|
||||
element: Element
|
||||
element: Element,
|
||||
astToDelta: (ast: HtmlAST) => RichTextType
|
||||
): TableBlockPropsSerialized => {
|
||||
const headerRows = getAllTag(element, 'thead').flatMap(node =>
|
||||
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'th'))
|
||||
@@ -184,33 +185,26 @@ export const parseTableFromHtml = (
|
||||
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td'))
|
||||
);
|
||||
const allRows = [...headerRows, ...bodyRows, ...footerRows];
|
||||
const rowTextLists: string[][] = [];
|
||||
const rowTextLists: RichTextType[][] = [];
|
||||
allRows.forEach(cells => {
|
||||
const row: string[] = [];
|
||||
const row: RichTextType[] = [];
|
||||
cells.forEach(cell => {
|
||||
row.push(getTextFromElement(cell));
|
||||
row.push(astToDelta(cell));
|
||||
});
|
||||
rowTextLists.push(row);
|
||||
});
|
||||
return createTableProps(rowTextLists);
|
||||
};
|
||||
|
||||
const getTextFromTableCell = (node: TableCell) => {
|
||||
const getTextFromPhrasingContent = (node: PhrasingContent) => {
|
||||
if (node.type === 'text') {
|
||||
return node.value;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
return node.children.map(child => getTextFromPhrasingContent(child)).join('');
|
||||
};
|
||||
|
||||
export const parseTableFromMarkdown = (node: MarkdownTable) => {
|
||||
const rowTextLists: string[][] = [];
|
||||
export const parseTableFromMarkdown = (
|
||||
node: MarkdownTable,
|
||||
astToDelta: (ast: MarkdownAST) => RichTextType
|
||||
) => {
|
||||
const rowTextLists: RichTextType[][] = [];
|
||||
node.children.forEach(row => {
|
||||
const rowText: string[] = [];
|
||||
const rowText: RichTextType[] = [];
|
||||
row.children.forEach(cell => {
|
||||
rowText.push(getTextFromTableCell(cell));
|
||||
rowText.push(astToDelta(cell));
|
||||
});
|
||||
rowTextLists.push(rowText);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lottiefiles/dotlottie-wc": "^0.4.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { type EditorHost, TextSelection } from '@blocksuite/block-std';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ export interface TextFormatConfig {
|
||||
hotkey?: string;
|
||||
activeWhen: (host: EditorHost) => boolean;
|
||||
action: (host: EditorHost) => void;
|
||||
textChecker?: (host: EditorHost) => boolean;
|
||||
}
|
||||
|
||||
export const textFormatConfigs: TextFormatConfig[] = [
|
||||
@@ -124,5 +125,14 @@ export const textFormatConfigs: TextFormatConfig[] = [
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleLink).run();
|
||||
},
|
||||
// should check text length
|
||||
textChecker: host => {
|
||||
const textSelection = host.std.selection.find(TextSelection);
|
||||
if (!textSelection || textSelection.isCollapsed()) return false;
|
||||
|
||||
return Boolean(
|
||||
textSelection.from.length + (textSelection.to?.length ?? 0)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+12
@@ -25,6 +25,7 @@ export interface FootNoteNodeConfig {
|
||||
customPopupRenderer?: FootNotePopupRenderer;
|
||||
interactive?: boolean;
|
||||
hidePopup?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
onPopupClick?: FootNotePopupClickHandler;
|
||||
}
|
||||
|
||||
@@ -33,7 +34,9 @@ export class FootNoteNodeConfigProvider {
|
||||
private _customPopupRenderer?: FootNotePopupRenderer;
|
||||
private _hidePopup: boolean;
|
||||
private _interactive: boolean;
|
||||
private _disableHoverEffect: boolean;
|
||||
private _onPopupClick?: FootNotePopupClickHandler;
|
||||
|
||||
get customNodeRenderer() {
|
||||
return this._customNodeRenderer;
|
||||
}
|
||||
@@ -58,6 +61,10 @@ export class FootNoteNodeConfigProvider {
|
||||
return this._interactive;
|
||||
}
|
||||
|
||||
get disableHoverEffect() {
|
||||
return this._disableHoverEffect;
|
||||
}
|
||||
|
||||
constructor(
|
||||
config: FootNoteNodeConfig,
|
||||
readonly std: BlockStdScope
|
||||
@@ -66,6 +73,7 @@ export class FootNoteNodeConfigProvider {
|
||||
this._customPopupRenderer = config.customPopupRenderer;
|
||||
this._hidePopup = config.hidePopup ?? false;
|
||||
this._interactive = config.interactive ?? true;
|
||||
this._disableHoverEffect = config.disableHoverEffect ?? false;
|
||||
this._onPopupClick = config.onPopupClick;
|
||||
}
|
||||
|
||||
@@ -85,6 +93,10 @@ export class FootNoteNodeConfigProvider {
|
||||
this._interactive = interactive;
|
||||
}
|
||||
|
||||
setDisableHoverEffect(disableHoverEffect: boolean) {
|
||||
this._disableHoverEffect = disableHoverEffect;
|
||||
}
|
||||
|
||||
setPopupClick(onPopupClick: FootNotePopupClickHandler) {
|
||||
this._onPopupClick = onPopupClick;
|
||||
}
|
||||
|
||||
+28
-3
@@ -19,6 +19,7 @@ import { shift } from '@floating-ui/dom';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ref } from 'lit-html/directives/ref.js';
|
||||
|
||||
import { HoverController } from '../../../../../hover/controller';
|
||||
@@ -37,7 +38,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
.footnote-content-default {
|
||||
display: inline-block;
|
||||
background: ${unsafeCSSVarV2('button/primary')};
|
||||
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
|
||||
color: ${unsafeCSSVarV2('button/pureWhiteText')};
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -48,6 +49,21 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
transition: background 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.footnote-node.hover-effect {
|
||||
.footnote-content-default {
|
||||
color: var(--affine-text-primary-color);
|
||||
background: ${unsafeCSSVarV2('block/footnote/numberBg')};
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-node.hover-effect:hover {
|
||||
.footnote-content-default {
|
||||
color: ${unsafeCSSVarV2('button/pureWhiteText')};
|
||||
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -67,6 +83,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return this.config?.hidePopup;
|
||||
}
|
||||
|
||||
get disableHoverEffect() {
|
||||
return this.config?.disableHoverEffect;
|
||||
}
|
||||
|
||||
get onPopupClick() {
|
||||
return this.config?.onPopupClick;
|
||||
}
|
||||
@@ -142,7 +162,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
},
|
||||
};
|
||||
},
|
||||
{ enterDelay: 500 }
|
||||
{ enterDelay: 300 }
|
||||
);
|
||||
|
||||
override render() {
|
||||
@@ -156,9 +176,14 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
? this.customNodeRenderer(footnote, this.std)
|
||||
: this._FootNoteDefaultContent(footnote);
|
||||
|
||||
const nodeClasses = classMap({
|
||||
'footnote-node': true,
|
||||
'hover-effect': !this.disableHoverEffect,
|
||||
});
|
||||
|
||||
return html`<span
|
||||
${this.hidePopup ? '' : ref(this._whenHover.setReference)}
|
||||
class="footnote-node"
|
||||
class=${nodeClasses}
|
||||
>${node}<v-text .str=${ZERO_WIDTH_NON_JOINER}></v-text
|
||||
></span>`;
|
||||
}
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ export class FootNotePopupChip extends LitElement {
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.prefix-icon,
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
.footnote-popup-container {
|
||||
border-radius: 4px;
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
border-radius: 4px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
@@ -20,8 +20,14 @@ export const textFormatKeymap = (std: BlockStdScope) =>
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (!textSelection) return;
|
||||
|
||||
const allowed = config.textChecker?.(std.host) ?? true;
|
||||
if (!allowed) return;
|
||||
|
||||
const event = ctx.get('keyboardState').raw;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
config.action(std.host);
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -3,17 +3,30 @@ import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const checkboxPropertyType = propertyType('checkbox');
|
||||
|
||||
const FALSE_VALUES = new Set([
|
||||
'false',
|
||||
'no',
|
||||
'0',
|
||||
'',
|
||||
'undefined',
|
||||
'null',
|
||||
'否',
|
||||
'不',
|
||||
'错',
|
||||
'错误',
|
||||
'取消',
|
||||
'关闭',
|
||||
]);
|
||||
|
||||
export const checkboxPropertyModelConfig =
|
||||
checkboxPropertyType.modelConfig<boolean>({
|
||||
name: 'Checkbox',
|
||||
type: () => t.boolean.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => (value ? 'True' : 'False'),
|
||||
cellFromString: ({ value }) => {
|
||||
return {
|
||||
value: value !== 'False',
|
||||
};
|
||||
},
|
||||
cellFromString: ({ value }) => ({
|
||||
value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()),
|
||||
}),
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) =>
|
||||
typeof value !== 'boolean' ? undefined : value,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@blocksuite/icons": "^2.2.1",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/icons": "^2.2.2",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.3",
|
||||
"@lottiefiles/dotlottie-wc": "^0.4.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
|
||||
@@ -3,15 +3,63 @@ edition = "2021"
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
doc-loader = ["docx-parser", "infer", "path-ext", "pdf-extract", "readability", "serde_json", "strum_macros", "text-splitter", "thiserror", "tree-sitter", "url"]
|
||||
tree-sitter = [
|
||||
"cc",
|
||||
"dep:tree-sitter",
|
||||
"dep:tree-sitter-c",
|
||||
"dep:tree-sitter-c-sharp",
|
||||
"dep:tree-sitter-cpp",
|
||||
"dep:tree-sitter-go",
|
||||
"dep:tree-sitter-java",
|
||||
"dep:tree-sitter-javascript",
|
||||
"dep:tree-sitter-kotlin-ng",
|
||||
"dep:tree-sitter-python",
|
||||
"dep:tree-sitter-rust",
|
||||
"dep:tree-sitter-scala",
|
||||
"dep:tree-sitter-typescript",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser", optional = true }
|
||||
infer = { version = "0.19.0", optional = true }
|
||||
path-ext = { version = "0.1.1", optional = true }
|
||||
pdf-extract = { version = "0.8.2", optional = true }
|
||||
readability = { version = "0.3.0", optional = true, default-features = false }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
strum_macros = { version = "0.26.2", optional = true }
|
||||
text-splitter = { version = "0.22", features = ["markdown", "tiktoken-rs"], optional = true }
|
||||
thiserror = { version = "1", optional = true }
|
||||
tree-sitter = { version = "0.25", optional = true }
|
||||
tree-sitter-c = { version = "0.23", optional = true }
|
||||
tree-sitter-c-sharp = { version = "0.23", optional = true }
|
||||
tree-sitter-cpp = { version = "0.23", optional = true }
|
||||
tree-sitter-go = { version = "0.23", optional = true }
|
||||
tree-sitter-java = { version = "0.23", optional = true }
|
||||
tree-sitter-javascript = { version = "0.23", optional = true }
|
||||
tree-sitter-kotlin-ng = { version = "1.1", optional = true }
|
||||
tree-sitter-python = { version = "0.23", optional = true }
|
||||
tree-sitter-rust = { version = "0.23", optional = true }
|
||||
tree-sitter-scala = { version = "0.23", optional = true }
|
||||
tree-sitter-typescript = { version = "0.23", optional = true }
|
||||
url = { version = "2.5", optional = true }
|
||||
|
||||
|
||||
tiktoken-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion2 = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1", optional = true }
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "hashcash"
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
# DOCX Demo
|
||||
|
||||
# <a name="OLE_LINK1"></a><a name="OLE_LINK2"></a><a name="_Toc359077851"></a>Demonstration of DOCX support in calibre
|
||||
|
||||
This document demonstrates the ability of the calibre DOCX Input plugin to convert the various typographic features in a Microsoft Word (2007 and newer) document. Convert this document to a modern ebook format, such as AZW3 for Kindles or EPUB for other ebook readers, to see it in action.
|
||||
|
||||
There is support for images, tables, lists, footnotes, endnotes, links, dropcaps and various types of text and paragraph level formatting.
|
||||
|
||||
To see the DOCX conversion in action, simply add this file to calibre using the **“Add Books” **button and then click “**Convert”. ** Set the output format in the top right corner of the conversion dialog to EPUB or AZW3 and click **“OK”**.
|
||||
|
||||
# <a name="_Toc359077852"></a>Text Formatting
|
||||
|
||||
## <a name="_Toc359077853"></a>Inline formatting
|
||||
|
||||
Here, we demonstrate various types of inline text formatting and the use of embedded fonts.
|
||||
|
||||
Here is some **bold, ***italic, ****bold-italic, ***__underlined __and ~~struck out ~~ text. Then, we have a superscript and a subscript. Now we see some red, green and blue text. Some text with a yellow highlight. Some text in a box. Some text in inverse video.
|
||||
|
||||
A paragraph with styled text: subtle emphasis followed by strong text and intense emphasis. This paragraph uses document wide styles for styling rather than inline text properties as demonstrated in the previous paragraph — calibre can handle both with equal ease.
|
||||
|
||||
## <a name="_Toc359077854"></a>Fun with fonts
|
||||
|
||||
This document has embedded the Ubuntu font family. The body text is in the Ubuntu typeface, here is some text in the Ubuntu Mono typeface, notice how every letter has the same width, even i and m. Every embedded font will automatically be embedded in the output ebook during conversion.
|
||||
|
||||
## ***<a name="_Paragraph_level_formatting"></a>******<a name="_Toc359077855"></a>******Paragraph level formatting***
|
||||
|
||||
You can do crazy things with paragraphs, if the urge strikes you. For instance this paragraph is right aligned and has a right border. It has also been given a light gray background.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
For the lovers of poetry amongst you, paragraphs with hanging indents, like this often come in handy. You can use hanging indents to ensure that a line of poetry retains its individual identity as a line even when the screen is too narrow to display it as a single line. Not only does this paragraph have a hanging indent, it is also has an extra top margin, setting it apart from the preceding paragraph.
|
||||
|
||||
# <a name="_Toc359077856"></a>Tables
|
||||
|
||||
| | |
|
||||
| ----------- | -------- |
|
||||
| ITEM | NEEDED |
|
||||
| Books | 1 |
|
||||
| Pens | 3 |
|
||||
| Pencils | 2 |
|
||||
| Highlighter | 2 colors |
|
||||
| Scissors | 1 pair |
|
||||
|
||||
Tables in Word can vary from the extremely simple to the extremely complex. calibre tries to do its best when converting tables. While you may run into trouble with the occasional table, the vast majority of common cases should be converted very well, as demonstrated in this section. Note that for optimum results, when creating tables in Word, you should set their widths using percentages, rather than absolute units. To the left of this paragraph is a floating two column table with a nice green border and header row.
|
||||
|
||||
Now let’s look at a fancier table—one with alternating row colors and partial borders. This table is stretched out to take 100% of the available width.
|
||||
|
||||
| | | | | | |
|
||||
| ------------ | ------- | ------- | ------- | ------- | ------- |
|
||||
| City or Town | Point A | Point B | Point C | Point D | Point E |
|
||||
| Point A | — | | | | |
|
||||
| Point B | 87 | — | | | |
|
||||
| Point C | 64 | 56 | — | | |
|
||||
| Point D | 37 | 32 | 91 | — | |
|
||||
| Point E | 93 | 35 | 54 | 43 | — |
|
||||
|
||||
Next, we see a table with special formatting in various locations. Notice how the formatting for the header row and sub header rows is preserved.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
| | | | |
|
||||
| ---------------- | ------------- | ------------------- | ------ |
|
||||
| College | New students | Graduating students | Change |
|
||||
| | Undergraduate | | |
|
||||
| Cedar University | 110 | 103 | +7 |
|
||||
| Oak Institute | 202 | 210 | -8 |
|
||||
| | Graduate | | |
|
||||
| Cedar University | 24 | 20 | +4 |
|
||||
| Elm College | 43 | 53 | -10 |
|
||||
| Total | 998 | 908 | 90 |
|
||||
|
||||
Source: Fictitious data, for illustration purposes only
|
||||
|
||||
Next, we have something a little more complex, a nested table, i.e. a table inside another table. Additionally, the inner table has some of its cells merged. The table is displayed horizontally centered.
|
||||
|
||||
| | |
|
||||
| --- | -------------------------------------------------------------- |
|
||||
| | To the left is a table inside a table, with some cells merged. |
|
||||
|
||||
We end with a fancy calendar, note how much of the original formatting is preserved. Note that this table will only display correctly on relatively wide screens. In general, very wide tables or tables whose cells have fixed width requirements don’t fare well in ebooks.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
| | | | | | | | | | | | | |
|
||||
| ------------- | | --- | | --- | | --- | | --- | | --- | | --- |
|
||||
| December 2007 | | | | | | | | | | | | |
|
||||
| Sun | | Mon | | Tue | | Wed | | Thu | | Fri | | Sat |
|
||||
| | | | | | | | | | | | | 1 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 2 | | 3 | | 4 | | 5 | | 6 | | 7 | | 8 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 9 | | 10 | | 11 | | 12 | | 13 | | 14 | | 15 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 16 | | 17 | | 18 | | 19 | | 20 | | 21 | | 22 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 23 | | 24 | | 25 | | 26 | | 27 | | 28 | | 29 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 30 | | 31 | | | | | | | | | | |
|
||||
|
||||
# <a name="_Toc359077857"></a>Structural Elements
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Miscellaneous structural elements you can add to your document, like footnotes, endnotes, dropcaps and the like.
|
||||
|
||||
## <a name="_Toc359077858"></a>Footnotes & Endnotes
|
||||
|
||||
Footnotes and endnotes are automatically recognized and both are converted to endnotes, with backlinks for maximum ease of use in ebook devices.
|
||||
|
||||
## <a name="_Toc359077859"></a>Dropcaps
|
||||
|
||||
D
|
||||
|
||||
rop caps are used to emphasize the leading paragraph at the start of a section. In Word it is possible to specify how many lines of text a drop-cap should use. Because of limitations in ebook technology, this is not possible when converting. Instead, the converted drop cap will use font size and line height to simulate the effect as well as possible. While not as good as the original, the result is usually tolerable. This paragraph has a “D” dropcap set to occupy three lines of text with a font size of 58.5 pts. Depending on the screen width and capabilities of the device you view the book on, this dropcap can look anything from perfect to ugly.
|
||||
|
||||
## <a name="_Toc359077860"></a>Links
|
||||
|
||||
Two kinds of links are possible, those that refer to an external website and those that refer to locations inside the document itself. Both are supported by calibre. For example, here is a link pointing to the [calibre download page](http://calibre-ebook.com/download). Then we have a link that points back to the section on [paragraph level formatting](#_Paragraph_level_formatting) in this document.
|
||||
|
||||
## <a name="_Toc359077861"></a>Table of Contents
|
||||
|
||||
There are two approaches that calibre takes when generating a Table of Contents. The first is if the Word document has a Table of Contents itself. Provided that the Table of Contents uses hyperlinks, calibre will automatically use it. The levels of the Table of Contents are identified by their left indent, so if you want the ebook to have a multi-level Table of Contents, make sure you create a properly indented Table of Contents in Word.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
If no Table of Contents is found in the document, then a table of contents is automatically generated from the headings in the document. A heading is identified as something that has the Heading 1 or Heading 2, etc. style applied to it. These headings are turned into a Table of Contents with Heading 1 being the topmost level, Heading 2 the second level and so on.
|
||||
|
||||
You can see the Table of Contents created by calibre by clicking the Table of Contents button in whatever viewer you are using to view the converted ebook.
|
||||
|
||||
# <a name="_Toc359077862"></a>Images
|
||||
|
||||
Images can be of three main types. Inline images are images that are part of the normal text flow, like this image of a green dot . Inline images do not cause breaks in the text and are usually small in size. The next category of image is a floating image, one that “floats “ on the page and is surrounded by text. Word supports more types of floating images than are possible with current ebook technology, so the conversion maps floating images to simple left and right floats, as you can see with the left and right arrow images on the sides of this paragraph.
|
||||
|
||||
The final type of image is a “block” image, one that becomes a paragraph on its own and has no text on either side. Below is a centered green dot.
|
||||
|
||||
Centered images like this are useful for large pictures that should be a focus of attention.
|
||||
|
||||
Generally, it is not possible to translate the exact positioning of images from a Word document to an ebook. That is because in Word, image positioning is specified in absolute units from the page boundaries. There is no analogous technology in ebooks, so the conversion will usually end up placing the image either centered or floating close to the point in the text where it was inserted, not necessarily where it appears on the page in Word.
|
||||
|
||||
# <a name="_Toc359077863"></a>Lists
|
||||
|
||||
All types of lists are supported by the conversion, with the exception of lists that use fancy bullets, these get converted to regular bullets.
|
||||
|
||||
## <a name="_Toc359077864"></a>Bulleted List
|
||||
|
||||
- One
|
||||
|
||||
- Two
|
||||
|
||||
## <a name="_Toc359077865"></a>Numbered List
|
||||
|
||||
1. One, with a very long line to demonstrate that the hanging indent for the list is working correctly
|
||||
|
||||
2. Two
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## <a name="_Toc359077866"></a>Multi-level Lists
|
||||
|
||||
1. One
|
||||
|
||||
2. Two
|
||||
|
||||
3. Three
|
||||
|
||||
4. Four with a very long line to demonstrate that the hanging indent for the list is working correctly.
|
||||
|
||||
5. Five
|
||||
|
||||
6. Six
|
||||
|
||||
A Multi-level list with bullets:
|
||||
|
||||
- One
|
||||
|
||||
- Two
|
||||
|
||||
- This bullet uses an image as the bullet item
|
||||
|
||||
- Four
|
||||
|
||||
- Five
|
||||
|
||||
## <a name="_Toc359077867"></a>Continued Lists
|
||||
|
||||
i. One
|
||||
|
||||
j. Two
|
||||
|
||||
An interruption in our regularly scheduled listing, for this essential and very relevant public service announcement.
|
||||
|
||||
k. We now resume our normal programming
|
||||
|
||||
l. Four
|
||||
@@ -0,0 +1,182 @@
|
||||
# DOCX Demo
|
||||
|
||||
# <a name="OLE_LINK1"></a><a name="OLE_LINK2"></a><a name="_Toc359077851"></a>Demonstration of DOCX support in calibre
|
||||
|
||||
This document demonstrates the ability of the calibre DOCX Input plugin to convert the various typographic features in a Microsoft Word (2007 and newer) document. Convert this document to a modern ebook format, such as AZW3 for Kindles or EPUB for other ebook readers, to see it in action.
|
||||
|
||||
There is support for images, tables, lists, footnotes, endnotes, links, dropcaps and various types of text and paragraph level formatting.
|
||||
|
||||
To see the DOCX conversion in action, simply add this file to calibre using the **“Add Books” **button and then click “**Convert”. ** Set the output format in the top right corner of the conversion dialog to EPUB or AZW3 and click **“OK”**.
|
||||
|
||||
# <a name="_Toc359077852"></a>Text Formatting
|
||||
|
||||
## <a name="_Toc359077853"></a>Inline formatting
|
||||
|
||||
Here, we demonstrate various types of inline text formatting and the use of embedded fonts.
|
||||
|
||||
Here is some **bold, ***italic, ****bold-italic, ***__underlined __and ~~struck out ~~ text. Then, we have a superscript and a subscript. Now we see some red, green and blue text. Some text with a yellow highlight. Some text in a box. Some text in inverse video.
|
||||
|
||||
A paragraph with styled text: subtle emphasis followed by strong text and intense emphasis. This paragraph uses document wide styles for styling rather than inline text properties as demonstrated in the previous paragraph — calibre can handle both with equal ease.
|
||||
|
||||
## <a name="_Toc359077854"></a>Fun with fonts
|
||||
|
||||
This document has embedded the Ubuntu font family. The body text is in the Ubuntu typeface, here is some text in the Ubuntu Mono typeface, notice how every letter has the same width, even i and m. Every embedded font will automatically be embedded in the output ebook during conversion.
|
||||
|
||||
## ***<a name="_Paragraph_level_formatting"></a>******<a name="_Toc359077855"></a>******Paragraph level formatting***
|
||||
|
||||
You can do crazy things with paragraphs, if the urge strikes you. For instance this paragraph is right aligned and has a right border. It has also been given a light gray background.
|
||||
|
||||
For the lovers of poetry amongst you, paragraphs with hanging indents, like this often come in handy. You can use hanging indents to ensure that a line of poetry retains its individual identity as a line even when the screen is too narrow to display it as a single line. Not only does this paragraph have a hanging indent, it is also has an extra top margin, setting it apart from the preceding paragraph.
|
||||
|
||||
# <a name="_Toc359077856"></a>Tables
|
||||
|
||||
| | |
|
||||
| ----------- | -------- |
|
||||
| ITEM | NEEDED |
|
||||
| Books | 1 |
|
||||
| Pens | 3 |
|
||||
| Pencils | 2 |
|
||||
| Highlighter | 2 colors |
|
||||
| Scissors | 1 pair |
|
||||
|
||||
Tables in Word can vary from the extremely simple to the extremely complex. calibre tries to do its best when converting tables. While you may run into trouble with the occasional table, the vast majority of common cases should be converted very well, as demonstrated in this section. Note that for optimum results, when creating tables in Word, you should set their widths using percentages, rather than absolute units. To the left of this paragraph is a floating two column table with a nice green border and header row.
|
||||
|
||||
Now let’s look at a fancier table—one with alternating row colors and partial borders. This table is stretched out to take 100% of the available width.
|
||||
|
||||
| | | | | | |
|
||||
| ------------ | ------- | ------- | ------- | ------- | ------- |
|
||||
| City or Town | Point A | Point B | Point C | Point D | Point E |
|
||||
| Point A | — | | | | |
|
||||
| Point B | 87 | — | | | |
|
||||
| Point C | 64 | 56 | — | | |
|
||||
| Point D | 37 | 32 | 91 | — | |
|
||||
| Point E | 93 | 35 | 54 | 43 | — |
|
||||
|
||||
Next, we see a table with special formatting in various locations. Notice how the formatting for the header row and sub header rows is preserved.
|
||||
|
||||
| | | | |
|
||||
| ---------------- | ------------- | ------------------- | ------ |
|
||||
| College | New students | Graduating students | Change |
|
||||
| | Undergraduate | | |
|
||||
| Cedar University | 110 | 103 | +7 |
|
||||
| Oak Institute | 202 | 210 | -8 |
|
||||
| | Graduate | | |
|
||||
| Cedar University | 24 | 20 | +4 |
|
||||
| Elm College | 43 | 53 | -10 |
|
||||
| Total | 998 | 908 | 90 |
|
||||
|
||||
Source: Fictitious data, for illustration purposes only
|
||||
|
||||
Next, we have something a little more complex, a nested table, i.e. a table inside another table. Additionally, the inner table has some of its cells merged. The table is displayed horizontally centered.
|
||||
|
||||
| | |
|
||||
| --- | -------------------------------------------------------------- |
|
||||
| | To the left is a table inside a table, with some cells merged. |
|
||||
|
||||
We end with a fancy calendar, note how much of the original formatting is preserved. Note that this table will only display correctly on relatively wide screens. In general, very wide tables or tables whose cells have fixed width requirements don’t fare well in ebooks.
|
||||
|
||||
| | | | | | | | | | | | | |
|
||||
| ------------- | | --- | | --- | | --- | | --- | | --- | | --- |
|
||||
| December 2007 | | | | | | | | | | | | |
|
||||
| Sun | | Mon | | Tue | | Wed | | Thu | | Fri | | Sat |
|
||||
| | | | | | | | | | | | | 1 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 2 | | 3 | | 4 | | 5 | | 6 | | 7 | | 8 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 9 | | 10 | | 11 | | 12 | | 13 | | 14 | | 15 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 16 | | 17 | | 18 | | 19 | | 20 | | 21 | | 22 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 23 | | 24 | | 25 | | 26 | | 27 | | 28 | | 29 |
|
||||
| | | | | | | | | | | | | |
|
||||
| 30 | | 31 | | | | | | | | | | |
|
||||
|
||||
# <a name="_Toc359077857"></a>Structural Elements
|
||||
|
||||
Miscellaneous structural elements you can add to your document, like footnotes, endnotes, dropcaps and the like.
|
||||
|
||||
## <a name="_Toc359077858"></a>Footnotes & Endnotes
|
||||
|
||||
Footnotes and endnotes are automatically recognized and both are converted to endnotes, with backlinks for maximum ease of use in ebook devices.
|
||||
|
||||
## <a name="_Toc359077859"></a>Dropcaps
|
||||
|
||||
D
|
||||
|
||||
rop caps are used to emphasize the leading paragraph at the start of a section. In Word it is possible to specify how many lines of text a drop-cap should use. Because of limitations in ebook technology, this is not possible when converting. Instead, the converted drop cap will use font size and line height to simulate the effect as well as possible. While not as good as the original, the result is usually tolerable. This paragraph has a “D” dropcap set to occupy three lines of text with a font size of 58.5 pts. Depending on the screen width and capabilities of the device you view the book on, this dropcap can look anything from perfect to ugly.
|
||||
|
||||
## <a name="_Toc359077860"></a>Links
|
||||
|
||||
Two kinds of links are possible, those that refer to an external website and those that refer to locations inside the document itself. Both are supported by calibre. For example, here is a link pointing to the [calibre download page](http://calibre-ebook.com/download). Then we have a link that points back to the section on [paragraph level formatting](#_Paragraph_level_formatting) in this document.
|
||||
|
||||
## <a name="_Toc359077861"></a>Table of Contents
|
||||
|
||||
There are two approaches that calibre takes when generating a Table of Contents. The first is if the Word document has a Table of Contents itself. Provided that the Table of Contents uses hyperlinks, calibre will automatically use it. The levels of the Table of Contents are identified by their left indent, so if you want the ebook to have a multi-level Table of Contents, make sure you create a properly indented Table of Contents in Word.
|
||||
|
||||
If no Table of Contents is found in the document, then a table of contents is automatically generated from the headings in the document. A heading is identified as something that has the Heading 1 or Heading 2, etc. style applied to it. These headings are turned into a Table of Contents with Heading 1 being the topmost level, Heading 2 the second level and so on.
|
||||
|
||||
You can see the Table of Contents created by calibre by clicking the Table of Contents button in whatever viewer you are using to view the converted ebook.
|
||||
|
||||
# <a name="_Toc359077862"></a>Images
|
||||
|
||||
Images can be of three main types. Inline images are images that are part of the normal text flow, like this image of a green dot . Inline images do not cause breaks in the text and are usually small in size. The next category of image is a floating image, one that “floats “ on the page and is surrounded by text. Word supports more types of floating images than are possible with current ebook technology, so the conversion maps floating images to simple left and right floats, as you can see with the left and right arrow images on the sides of this paragraph.
|
||||
|
||||
The final type of image is a “block” image, one that becomes a paragraph on its own and has no text on either side. Below is a centered green dot.
|
||||
|
||||
Centered images like this are useful for large pictures that should be a focus of attention.
|
||||
|
||||
Generally, it is not possible to translate the exact positioning of images from a Word document to an ebook. That is because in Word, image positioning is specified in absolute units from the page boundaries. There is no analogous technology in ebooks, so the conversion will usually end up placing the image either centered or floating close to the point in the text where it was inserted, not necessarily where it appears on the page in Word.
|
||||
|
||||
# <a name="_Toc359077863"></a>Lists
|
||||
|
||||
All types of lists are supported by the conversion, with the exception of lists that use fancy bullets, these get converted to regular bullets.
|
||||
|
||||
## <a name="_Toc359077864"></a>Bulleted List
|
||||
|
||||
- One
|
||||
|
||||
- Two
|
||||
|
||||
## <a name="_Toc359077865"></a>Numbered List
|
||||
|
||||
1. One, with a very long line to demonstrate that the hanging indent for the list is working correctly
|
||||
|
||||
2. Two
|
||||
|
||||
## <a name="_Toc359077866"></a>Multi-level Lists
|
||||
|
||||
1. One
|
||||
|
||||
2. Two
|
||||
|
||||
3. Three
|
||||
|
||||
4. Four with a very long line to demonstrate that the hanging indent for the list is working correctly.
|
||||
|
||||
5. Five
|
||||
|
||||
6. Six
|
||||
|
||||
A Multi-level list with bullets:
|
||||
|
||||
- One
|
||||
|
||||
- Two
|
||||
|
||||
- This bullet uses an image as the bullet item
|
||||
|
||||
- Four
|
||||
|
||||
- Five
|
||||
|
||||
## <a name="_Toc359077867"></a>Continued Lists
|
||||
|
||||
i. One
|
||||
|
||||
j. Two
|
||||
|
||||
An interruption in our regularly scheduled listing, for this essential and very relevant public service announcement.
|
||||
|
||||
k. We now resume our normal programming
|
||||
|
||||
l. Four
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#include <stdio.h>
|
||||
@@ -0,0 +1,4 @@
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
<!doctype html>
|
||||
<!-- saved from url=(0020)https://example.org/ -->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Example Domain</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #f0f0f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
div {
|
||||
width: 600px;
|
||||
margin: 5em auto;
|
||||
padding: 2em;
|
||||
background-color: #fdfdff;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 2px 3px 7px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
a:link,
|
||||
a:visited {
|
||||
color: #38488f;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
div {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>Example Domain</h1>
|
||||
<p>
|
||||
This domain is for use in illustrative examples in documents. You may
|
||||
use this domain in literature without prior coordination or asking for
|
||||
permission.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.iana.org/domains/example"
|
||||
rel="noreferrer"
|
||||
data-ss1736873651="1"
|
||||
>More information...</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
(function (
|
||||
canvas,
|
||||
canvasfont,
|
||||
audioblock,
|
||||
battery,
|
||||
webgl,
|
||||
webrtcdevice,
|
||||
gamepad,
|
||||
webvr,
|
||||
bluetooth,
|
||||
timezone,
|
||||
clientrects,
|
||||
clipboard,
|
||||
browserplugins
|
||||
) {
|
||||
function processFunctions(scope) {
|
||||
/* Browser Plugins */
|
||||
if (browserplugins == 'true') {
|
||||
scope.Object.defineProperty(navigator, 'plugins', {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function () {
|
||||
var browserplugins_triggerblock =
|
||||
scope.document.createElement('div');
|
||||
browserplugins_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_browserplugins';
|
||||
browserplugins_triggerblock.title = 'navigator.plugins';
|
||||
document.documentElement.appendChild(browserplugins_triggerblock);
|
||||
return '';
|
||||
},
|
||||
});
|
||||
}
|
||||
/* Canvas */
|
||||
if (canvas != 'false') {
|
||||
var fakecanvas = scope.document.createElement('canvas');
|
||||
fakecanvas.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_canvas';
|
||||
if (canvas == 'random') {
|
||||
var fakewidth = (fakecanvas.width =
|
||||
Math.floor(Math.random() * 999) + 1);
|
||||
var fakeheight = (fakecanvas.height =
|
||||
Math.floor(Math.random() * 999) + 1);
|
||||
}
|
||||
var canvas_a = scope.HTMLCanvasElement;
|
||||
var origToDataURL = canvas_a.prototype.toDataURL;
|
||||
var origToBlob = canvas_a.prototype.toBlob;
|
||||
canvas_a.prototype.toDataURL = function () {
|
||||
fakecanvas.title = 'toDataURL';
|
||||
document.documentElement.appendChild(fakecanvas);
|
||||
if (canvas == 'block') return false;
|
||||
else if (canvas == 'blank') {
|
||||
fakecanvas.width = this.width;
|
||||
fakecanvas.height = this.height;
|
||||
return origToDataURL.apply(fakecanvas, arguments);
|
||||
} else if (canvas == 'random') {
|
||||
return origToDataURL.apply(fakecanvas, arguments);
|
||||
}
|
||||
};
|
||||
canvas_a.prototype.toBlob = function () {
|
||||
fakecanvas.title = 'toBlob';
|
||||
document.documentElement.appendChild(fakecanvas);
|
||||
if (canvas == 'block') return false;
|
||||
else if (canvas == 'blank') {
|
||||
fakecanvas.width = this.width;
|
||||
fakecanvas.height = this.height;
|
||||
return origToBlob.apply(fakecanvas, arguments);
|
||||
} else if (canvas == 'random') {
|
||||
return origToBlob.apply(fakecanvas, arguments);
|
||||
}
|
||||
};
|
||||
var canvas_b = scope.CanvasRenderingContext2D;
|
||||
var origGetImageData = canvas_b.prototype.getImageData;
|
||||
canvas_b.prototype.getImageData = function () {
|
||||
fakecanvas.title = 'getImageData';
|
||||
document.documentElement.appendChild(fakecanvas);
|
||||
if (canvas == 'block') return false;
|
||||
else if (canvas == 'blank') {
|
||||
fakecanvas.width = this.width;
|
||||
fakecanvas.height = this.height;
|
||||
return origGetImageData.apply(
|
||||
fakecanvas.getContext('2d'),
|
||||
arguments
|
||||
);
|
||||
} else if (canvas == 'random') {
|
||||
return origGetImageData.apply(fakecanvas.getContext('2d'), [
|
||||
Math.floor(Math.random() * fakewidth) + 1,
|
||||
Math.floor(Math.random() * fakeheight) + 1,
|
||||
Math.floor(Math.random() * fakewidth) + 1,
|
||||
Math.floor(Math.random() * fakeheight) + 1,
|
||||
]);
|
||||
}
|
||||
};
|
||||
var origGetLineDash = canvas_b.prototype.getLineDash;
|
||||
canvas_b.prototype.getLineDash = function () {
|
||||
fakecanvas.title = 'getLineDash';
|
||||
document.documentElement.appendChild(fakecanvas);
|
||||
if (canvas == 'block') return false;
|
||||
else if (canvas == 'blank') {
|
||||
fakecanvas.width = this.width;
|
||||
fakecanvas.height = this.height;
|
||||
return origGetLineDash.apply(fakecanvas.getContext('2d'), [0, 0]);
|
||||
} else if (canvas == 'random') {
|
||||
return origGetLineDash.apply(fakecanvas.getContext('2d'), [
|
||||
Math.floor(Math.random() * fakewidth) + 1,
|
||||
Math.floor(Math.random() * fakeheight) + 1,
|
||||
]);
|
||||
}
|
||||
};
|
||||
var canvas_c = scope.WebGLRenderingContext;
|
||||
var origReadPixels = canvas_c.prototype.readPixels;
|
||||
canvas_c.prototype.readPixels = function () {
|
||||
fakecanvas.title = 'readPixels';
|
||||
document.documentElement.appendChild(fakecanvas);
|
||||
if (canvas == 'block') return false;
|
||||
else if (canvas == 'blank') {
|
||||
fakecanvas.width = this.width;
|
||||
fakecanvas.height = this.height;
|
||||
return origReadPixels.apply(
|
||||
fakecanvas.getContext('webgl'),
|
||||
arguments
|
||||
);
|
||||
} else if (canvas == 'random') {
|
||||
return origReadPixels.apply(fakecanvas.getContext('webgl'), [
|
||||
Math.floor(Math.random() * fakewidth) + 1,
|
||||
Math.floor(Math.random() * fakeheight) + 1,
|
||||
Math.floor(Math.random() * fakewidth) + 1,
|
||||
Math.floor(Math.random() * fakeheight) + 1,
|
||||
arguments[4],
|
||||
arguments[5],
|
||||
arguments[6],
|
||||
]);
|
||||
}
|
||||
};
|
||||
}
|
||||
/* Audio Block */
|
||||
if (audioblock == 'true') {
|
||||
var audioblock_triggerblock = scope.document.createElement('div');
|
||||
audioblock_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_audio';
|
||||
var audioblock_a = scope.AudioBuffer;
|
||||
audioblock_a.prototype.copyFromChannel = function () {
|
||||
audioblock_triggerblock.title = 'copyFromChannel';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
audioblock_a.prototype.getChannelData = function () {
|
||||
audioblock_triggerblock.title = 'getChannelData';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
var audioblock_b = scope.AnalyserNode;
|
||||
audioblock_b.prototype.getFloatFrequencyData = function () {
|
||||
audioblock_triggerblock.title = 'getFloatFrequencyData';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
audioblock_b.prototype.getByteFrequencyData = function () {
|
||||
audioblock_triggerblock.title = 'getByteFrequencyData';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
audioblock_b.prototype.getFloatTimeDomainData = function () {
|
||||
audioblock_triggerblock.title = 'getFloatTimeDomainData';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
audioblock_b.prototype.getByteTimeDomainData = function () {
|
||||
audioblock_triggerblock.title = 'getByteTimeDomainData';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
var audioblock_c = scope;
|
||||
audioblock_c.AudioContext = function () {
|
||||
audioblock_triggerblock.title = 'AudioContext';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
audioblock_c.webkitAudioContext = function () {
|
||||
audioblock_triggerblock.title = 'webkitAudioContext';
|
||||
document.documentElement.appendChild(audioblock_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/* Canvas Font */
|
||||
if (canvasfont == 'true') {
|
||||
var canvasfont_triggerblock = scope.document.createElement('div');
|
||||
canvasfont_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_canvasfont';
|
||||
var canvasfont_a = scope.CanvasRenderingContext2D;
|
||||
canvasfont_a.prototype.measureText = function () {
|
||||
canvasfont_triggerblock.title = 'measureText';
|
||||
document.documentElement.appendChild(canvasfont_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/* Battery */
|
||||
if (battery == 'true') {
|
||||
var battery_triggerblock = scope.document.createElement('div');
|
||||
battery_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_battery';
|
||||
var battery_a = scope.navigator;
|
||||
battery_a.getBattery = function () {
|
||||
battery_triggerblock.title = 'getBattery';
|
||||
document.documentElement.appendChild(battery_triggerblock);
|
||||
return void 0;
|
||||
};
|
||||
}
|
||||
/* WebGL */
|
||||
if (webgl == 'true') {
|
||||
var webgl_triggerblock = scope.document.createElement('div');
|
||||
webgl_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_webgl';
|
||||
var webgl_a = scope.HTMLCanvasElement;
|
||||
var origGetContext = webgl_a.prototype.getContext;
|
||||
webgl_a.prototype.getContext = function (arg) {
|
||||
if (arg.match(/webgl/i)) {
|
||||
webgl_triggerblock.title = 'getContext';
|
||||
document.documentElement.appendChild(webgl_triggerblock);
|
||||
return false;
|
||||
}
|
||||
return origGetContext.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
/* WebRTC */
|
||||
if (webrtcdevice == 'true') {
|
||||
var webrtc_triggerblock = scope.document.createElement('div');
|
||||
webrtc_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_webrtc';
|
||||
var webrtc_a = scope.MediaStreamTrack;
|
||||
webrtc_a.getSources = function () {
|
||||
webrtc_triggerblock.title = 'getSources';
|
||||
document.documentElement.appendChild(webrtc_triggerblock);
|
||||
return false;
|
||||
};
|
||||
webrtc_a.getMediaDevices = function () {
|
||||
webrtc_triggerblock.title = 'getMediaDevices';
|
||||
document.documentElement.appendChild(webrtc_triggerblock);
|
||||
return false;
|
||||
};
|
||||
var webrtc_b = scope.navigator.mediaDevices;
|
||||
webrtc_b.enumerateDevices = function () {
|
||||
webrtc_triggerblock.title = 'enumerateDevices';
|
||||
document.documentElement.appendChild(webrtc_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/* Gamepad */
|
||||
if (gamepad == 'true') {
|
||||
var gamepad_triggerblock = scope.document.createElement('div');
|
||||
gamepad_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_gamepad';
|
||||
var gamepad_a = scope.navigator;
|
||||
gamepad_a.getGamepads = function () {
|
||||
gamepad_triggerblock.title = 'getGamepads';
|
||||
document.documentElement.appendChild(gamepad_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/* WebVR */
|
||||
if (webvr == 'true') {
|
||||
var webvr_triggerblock = scope.document.createElement('div');
|
||||
webvr_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_webvr';
|
||||
var webvr_a = scope.navigator;
|
||||
webvr_a.getVRDisplays = function () {
|
||||
webvr_triggerblock.title = 'getVRDisplays';
|
||||
document.documentElement.appendChild(webvr_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/* Bluetooth */
|
||||
if (bluetooth == 'true') {
|
||||
if (scope.navigator.bluetooth) {
|
||||
var bluetooth_triggerblock = scope.document.createElement('div');
|
||||
bluetooth_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_bluetooth';
|
||||
var bluetooth_a = scope.navigator.bluetooth;
|
||||
bluetooth_a.requestDevice = function () {
|
||||
bluetooth_triggerblock.title = 'requestDevice';
|
||||
document.documentElement.appendChild(bluetooth_triggerblock);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
/* Client Rectangles */
|
||||
if (clientrects == 'true') {
|
||||
var clientrects_triggerblock = scope.document.createElement('div');
|
||||
clientrects_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_clientrects';
|
||||
Element.prototype.getClientRects = function () {
|
||||
clientrects_triggerblock.title = 'getClientRects';
|
||||
document.documentElement.appendChild(clientrects_triggerblock);
|
||||
return [
|
||||
{ top: 0, bottom: 0, left: 0, right: 0, height: 0, width: 0 },
|
||||
];
|
||||
};
|
||||
}
|
||||
/* Timezone */
|
||||
if (timezone != 'false') {
|
||||
var timezone_triggerblock = scope.document.createElement('div');
|
||||
timezone_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_timezone';
|
||||
var timezone_a = scope.Date;
|
||||
timezone_a.prototype.getTimezoneOffset = function () {
|
||||
timezone_triggerblock.title = 'getTimezoneOffset';
|
||||
document.documentElement.appendChild(timezone_triggerblock);
|
||||
if (timezone == 'random')
|
||||
return [
|
||||
'720',
|
||||
'660',
|
||||
'600',
|
||||
'570',
|
||||
'540',
|
||||
'480',
|
||||
'420',
|
||||
'360',
|
||||
'300',
|
||||
'240',
|
||||
'210',
|
||||
'180',
|
||||
'120',
|
||||
'60',
|
||||
'0',
|
||||
'-60',
|
||||
'-120',
|
||||
'-180',
|
||||
'-210',
|
||||
'-240',
|
||||
'-270',
|
||||
'-300',
|
||||
'-330',
|
||||
'-345',
|
||||
'-360',
|
||||
'-390',
|
||||
'-420',
|
||||
'-480',
|
||||
'-510',
|
||||
'-525',
|
||||
'-540',
|
||||
'-570',
|
||||
'-600',
|
||||
'-630',
|
||||
'-660',
|
||||
'-720',
|
||||
'-765',
|
||||
'-780',
|
||||
'-840',
|
||||
][Math.floor(Math.random() * 39)];
|
||||
return timezone;
|
||||
};
|
||||
}
|
||||
/* Clipboard */
|
||||
if (clipboard == 'true') {
|
||||
var clipboard_triggerblock = scope.document.createElement('div');
|
||||
clipboard_triggerblock.className =
|
||||
'scriptsafe_oiigbmnaadbkfbmpbfijlflahbdbdgdf_clipboard';
|
||||
var clipboard_a = document;
|
||||
var origExecCommand = clipboard_a.execCommand;
|
||||
clipboard_a.execCommand = function () {
|
||||
clipboard_triggerblock.title = 'execCommand';
|
||||
document.documentElement.appendChild(clipboard_triggerblock);
|
||||
if (arguments[0] == 'cut' || arguments[0] == 'copy') return false;
|
||||
return origExecCommand.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
processFunctions(window);
|
||||
var iwin = HTMLIFrameElement.prototype.__lookupGetter__('contentWindow'),
|
||||
idoc = HTMLIFrameElement.prototype.__lookupGetter__('contentDocument');
|
||||
Object.defineProperties(HTMLIFrameElement.prototype, {
|
||||
contentWindow: {
|
||||
get: function () {
|
||||
var frame = iwin.apply(this);
|
||||
if (
|
||||
this.src &&
|
||||
this.src.indexOf('//') != -1 &&
|
||||
location.host != this.src.split('/')[2]
|
||||
)
|
||||
return frame;
|
||||
try {
|
||||
frame.HTMLCanvasElement;
|
||||
} catch (err) {
|
||||
/* do nothing*/
|
||||
}
|
||||
processFunctions(frame);
|
||||
return frame;
|
||||
},
|
||||
},
|
||||
contentDocument: {
|
||||
get: function () {
|
||||
if (
|
||||
this.src &&
|
||||
this.src.indexOf('//') != -1 &&
|
||||
location.host != this.src.split('/')[2]
|
||||
)
|
||||
return idoc.apply(this);
|
||||
var frame = iwin.apply(this);
|
||||
try {
|
||||
frame.HTMLCanvasElement;
|
||||
} catch (err) {
|
||||
/* do nothing*/
|
||||
}
|
||||
processFunctions(frame);
|
||||
return idoc.apply(this);
|
||||
},
|
||||
},
|
||||
});
|
||||
})(
|
||||
'block',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'true',
|
||||
'false',
|
||||
'true',
|
||||
'true',
|
||||
'true'
|
||||
);
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
Example Domain
|
||||
|
||||
This domain is for use in illustrative examples in documents. You may
|
||||
use this domain in literature without prior coordination or asking for
|
||||
permission.
|
||||
More information...
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
Sample PDF
|
||||
This is a simple PDF file. Fun fun fun.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus facilisis odio sed mi.
|
||||
Curabitur suscipit. Nullam vel nisi. Etiam semper ipsum ut lectus. Proin aliquam, erat eget
|
||||
pharetra commodo, eros mi condimentum quam, sed commodo justo quam ut velit.
|
||||
Integer a erat. Cras laoreet ligula cursus enim. Aenean scelerisque velit et tellus.
|
||||
Vestibulum dictum aliquet sem. Nulla facilisi. Vestibulum accumsan ante vitae elit. Nulla
|
||||
erat dolor, blandit in, rutrum quis, semper pulvinar, enim. Nullam varius congue risus.
|
||||
Vivamus sollicitudin, metus ut interdum eleifend, nisi tellus pellentesque elit, tristique
|
||||
accumsan eros quam et risus. Suspendisse libero odio, mattis sit amet, aliquet eget,
|
||||
hendrerit vel, nulla. Sed vitae augue. Aliquam erat volutpat. Aliquam feugiat vulputate nisl.
|
||||
Suspendisse quis nulla pretium ante pretium mollis. Proin velit ligula, sagittis at, egestas a,
|
||||
pulvinar quis, nisl.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Pellentesque sit amet lectus. Praesent pulvinar, nunc quis iaculis sagittis, justo quam
|
||||
lobortis tortor, sed vestibulum dui metus venenatis est. Nunc cursus ligula. Nulla facilisi.
|
||||
Phasellus ullamcorper consectetuer ante. Duis tincidunt, urna id condimentum luctus, nibh
|
||||
ante vulputate sapien, id sagittis massa orci ut enim. Pellentesque vestibulum convallis
|
||||
sem. Nulla consequat quam ut nisl. Nullam est. Curabitur tincidunt dapibus lorem. Proin
|
||||
velit turpis, scelerisque sit amet, iaculis nec, rhoncus ac, ipsum. Phasellus lorem arcu,
|
||||
feugiat eu, gravida eu, consequat molestie, ipsum. Nullam vel est ut ipsum volutpat
|
||||
feugiat. Aenean pellentesque.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
In mauris. Pellentesque dui nisi, iaculis eu, rhoncus in, venenatis ac, ante. Ut odio justo,
|
||||
scelerisque vel, facilisis non, commodo a, pede. Cras nec massa sit amet tortor volutpat
|
||||
varius. Donec lacinia, neque a luctus aliquet, pede massa imperdiet ante, at varius lorem
|
||||
pede sed sapien. Fusce erat nibh, aliquet in, eleifend eget, commodo eget, erat. Fusce
|
||||
consectetuer. Cras risus tortor, porttitor nec, tristique sed, convallis semper, eros. Fusce
|
||||
vulputate ipsum a mauris. Phasellus mollis. Curabitur sed urna. Aliquam nec sapien non
|
||||
nibh pulvinar convallis. Vivamus facilisis augue quis quam. Proin cursus aliquet metus.
|
||||
Suspendisse lacinia. Nulla at tellus ac turpis eleifend scelerisque. Maecenas a pede vitae
|
||||
enim commodo interdum. Donec odio. Sed sollicitudin dui vitae justo.
|
||||
|
||||
Morbi elit nunc, facilisis a, mollis a, molestie at, lectus. Suspendisse eget mauris eu tellus
|
||||
molestie cursus. Duis ut magna at justo dignissim condimentum. Cum sociis natoque
|
||||
penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus varius. Ut sit
|
||||
amet diam suscipit mauris ornare aliquam. Sed varius. Duis arcu. Etiam tristique massa
|
||||
eget dui. Phasellus congue. Aenean est erat, tincidunt eget, venenatis quis, commodo at,
|
||||
quam.
|
||||
@@ -0,0 +1,10 @@
|
||||
fn factorial(n: u64) -> u64 {
|
||||
if n == 0 {
|
||||
return 1;
|
||||
}
|
||||
n * factorial(n - 1)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
fn factorial(n: u64) -> u64 {
|
||||
if n == 0 {
|
||||
return 1;
|
||||
}
|
||||
n * factorial(n - 1)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function sample() {
|
||||
return 'sample';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function sample() {
|
||||
return 'sample';
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::{io::Cursor, path::PathBuf};
|
||||
|
||||
use path_ext::PathExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Chunk {
|
||||
pub index: usize,
|
||||
pub content: String,
|
||||
pub start: Option<usize>,
|
||||
pub end: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct DocOptions {
|
||||
code_threshold: u64,
|
||||
}
|
||||
|
||||
impl Default for DocOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code_threshold: 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Doc {
|
||||
pub name: String,
|
||||
pub chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
pub fn new(file_path: &str, doc: &[u8]) -> Option<Self> {
|
||||
Self::with_options(file_path, doc, DocOptions::default())
|
||||
}
|
||||
|
||||
pub fn with_options(file_path: &str, doc: &[u8], options: DocOptions) -> Option<Self> {
|
||||
if let Some(kind) =
|
||||
infer::get(&doc[..4096.min(doc.len())]).or(infer::get_from_path(file_path).ok().flatten())
|
||||
{
|
||||
if kind.extension() == "pdf" {
|
||||
return Self::load_pdf(file_path, doc);
|
||||
} else if kind.extension() == "docx" {
|
||||
return Self::load_docx(file_path, doc);
|
||||
} else if kind.extension() == "html" {
|
||||
return Self::load_html(file_path, doc);
|
||||
}
|
||||
} else if let Ok(string) = String::from_utf8(doc.to_vec()).or_else(|_| {
|
||||
String::from_utf16(
|
||||
&doc
|
||||
.chunks_exact(2)
|
||||
.map(|b| u16::from_le_bytes([b[0], b[1]]))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}) {
|
||||
let path = PathBuf::from(file_path);
|
||||
match path.ext_str() {
|
||||
"md" => {
|
||||
let loader = TextLoader::new(string);
|
||||
let splitter = MarkdownSplitter::default();
|
||||
return Self::from_loader(file_path, loader, splitter).ok();
|
||||
}
|
||||
"rs" | "c" | "cpp" | "h" | "hpp" | "js" | "ts" | "tsx" | "go" | "py" => {
|
||||
let name = path.full_str().to_string();
|
||||
let loader =
|
||||
SourceCodeLoader::from_string(string).with_parser_option(LanguageParserOptions {
|
||||
language: get_language_by_filename(&name).ok()?,
|
||||
parser_threshold: options.code_threshold,
|
||||
});
|
||||
let splitter = TokenSplitter::default();
|
||||
return Self::from_loader(file_path, loader, splitter).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let loader = TextLoader::new(string);
|
||||
let splitter = TokenSplitter::default();
|
||||
return Self::from_loader(file_path, loader, splitter).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn from_loader(
|
||||
file_path: &str,
|
||||
loader: impl Loader,
|
||||
splitter: impl TextSplitter + 'static,
|
||||
) -> Result<Doc, LoaderError> {
|
||||
let name = file_path.to_string();
|
||||
let chunks = Self::get_chunks_from_loader(loader, splitter)?;
|
||||
Ok(Self { name, chunks })
|
||||
}
|
||||
|
||||
fn get_chunks_from_loader(
|
||||
loader: impl Loader,
|
||||
splitter: impl TextSplitter + 'static,
|
||||
) -> Result<Vec<Chunk>, LoaderError> {
|
||||
let docs = loader.load_and_split(splitter)?;
|
||||
Ok(
|
||||
docs
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, d)| Chunk {
|
||||
index,
|
||||
content: d.page_content,
|
||||
..Chunk::default()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_docx(file_path: &str, doc: &[u8]) -> Option<Self> {
|
||||
let loader = DocxLoader::new(Cursor::new(doc))?;
|
||||
let splitter = TokenSplitter::default();
|
||||
Self::from_loader(file_path, loader, splitter).ok()
|
||||
}
|
||||
|
||||
fn load_html(file_path: &str, doc: &[u8]) -> Option<Self> {
|
||||
let loader = HtmlLoader::from_string(
|
||||
String::from_utf8(doc.to_vec()).ok()?,
|
||||
Url::parse(file_path)
|
||||
.or(Url::parse("https://example.com/"))
|
||||
.ok()?,
|
||||
);
|
||||
let splitter = TokenSplitter::default();
|
||||
Self::from_loader(file_path, loader, splitter).ok()
|
||||
}
|
||||
|
||||
fn load_pdf(file_path: &str, doc: &[u8]) -> Option<Self> {
|
||||
let loader = PdfExtractLoader::new(Cursor::new(doc)).ok()?;
|
||||
let splitter = TokenSplitter::default();
|
||||
Self::from_loader(file_path, loader, splitter).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs::{read, read_to_string},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const FIXTURES: [&str; 6] = [
|
||||
"demo.docx",
|
||||
"sample.pdf",
|
||||
"sample.html",
|
||||
"sample.rs",
|
||||
"sample.c",
|
||||
"sample.ts",
|
||||
];
|
||||
|
||||
fn get_fixtures() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixtures() {
|
||||
let fixtures = get_fixtures();
|
||||
for fixture in FIXTURES.iter() {
|
||||
let buffer = read(fixtures.join(fixture)).unwrap();
|
||||
let doc = Doc::with_options(fixture, &buffer, DocOptions { code_threshold: 0 }).unwrap();
|
||||
for chunk in doc.chunks.iter() {
|
||||
let output =
|
||||
read_to_string(fixtures.join(format!("{}.{}.md", fixture, chunk.index))).unwrap();
|
||||
assert_eq!(chunk.content, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use docx_parser::MarkdownDocument;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DocxLoader {
|
||||
document: MarkdownDocument,
|
||||
}
|
||||
|
||||
impl DocxLoader {
|
||||
pub fn new<R: Read + Seek>(reader: R) -> Option<Self> {
|
||||
Some(Self {
|
||||
document: MarkdownDocument::from_reader(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_text(&self) -> String {
|
||||
self.document.to_markdown(false)
|
||||
}
|
||||
|
||||
fn extract_text_to_doc(&self) -> Document {
|
||||
Document::new(self.extract_text())
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for DocxLoader {
|
||||
fn load(self) -> Result<Vec<Document>, LoaderError> {
|
||||
let doc = self.extract_text_to_doc();
|
||||
Ok(vec![doc])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::read, io::Cursor, path::PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_fixtures_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_docx() {
|
||||
let docx_buffer = include_bytes!("../../../fixtures/demo.docx");
|
||||
let parsed_buffer = include_str!("../../../fixtures/demo.docx.md");
|
||||
|
||||
{
|
||||
let loader = DocxLoader::new(Cursor::new(docx_buffer)).unwrap();
|
||||
|
||||
let documents = loader.load().unwrap();
|
||||
|
||||
assert_eq!(documents.len(), 1);
|
||||
assert_eq!(documents[0].page_content, parsed_buffer);
|
||||
}
|
||||
|
||||
{
|
||||
let loader = DocxLoader::new(Cursor::new(docx_buffer)).unwrap();
|
||||
let documents = loader.load_and_split(TokenSplitter::default()).unwrap();
|
||||
|
||||
for (idx, doc) in documents.into_iter().enumerate() {
|
||||
assert_eq!(
|
||||
doc.page_content,
|
||||
String::from_utf8_lossy(
|
||||
&read(get_fixtures_path().join(format!("demo.docx.{}.md", idx))).unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use std::{io, str::Utf8Error, string::FromUtf8Error};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LoaderError {
|
||||
#[error("{0}")]
|
||||
TextSplitterError(#[from] TextSplitterError),
|
||||
|
||||
#[error(transparent)]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Utf8Error(#[from] Utf8Error),
|
||||
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] FromUtf8Error),
|
||||
|
||||
#[cfg(feature = "pdf-extract")]
|
||||
#[error(transparent)]
|
||||
PdfExtractError(#[from] pdf_extract::Error),
|
||||
|
||||
#[cfg(feature = "pdf-extract")]
|
||||
#[error(transparent)]
|
||||
PdfExtractOutputError(#[from] pdf_extract::OutputError),
|
||||
|
||||
#[error(transparent)]
|
||||
ReadabilityError(#[from] readability::error::Error),
|
||||
|
||||
#[error("Unsupported source language")]
|
||||
UnsupportedLanguage,
|
||||
|
||||
#[error("Error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
pub type LoaderResult<T> = Result<T, LoaderError>;
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
use super::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HtmlLoader<R> {
|
||||
html: R,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl HtmlLoader<Cursor<Vec<u8>>> {
|
||||
pub fn from_string<S: Into<String>>(input: S, url: Url) -> Self {
|
||||
let input = input.into();
|
||||
let reader = Cursor::new(input.into_bytes());
|
||||
Self::new(reader, url)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> HtmlLoader<R> {
|
||||
pub fn new(html: R, url: Url) -> Self {
|
||||
Self { html, url }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Send + Sync + 'static> Loader for HtmlLoader<R> {
|
||||
fn load(mut self) -> Result<Vec<Document>, LoaderError> {
|
||||
let cleaned_html = readability::extractor::extract(&mut self.html, &self.url)?;
|
||||
let doc =
|
||||
Document::new(format!("{}\n{}", cleaned_html.title, cleaned_html.text)).with_metadata(
|
||||
HashMap::from([("source".to_string(), Value::from(self.url.as_str()))]),
|
||||
);
|
||||
|
||||
Ok(vec![doc])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_html_loader() {
|
||||
let input = "<p>Hello world!</p>";
|
||||
|
||||
let html_loader = HtmlLoader::new(
|
||||
input.as_bytes(),
|
||||
Url::parse("https://example.com/").unwrap(),
|
||||
);
|
||||
|
||||
let documents = html_loader.load().unwrap();
|
||||
|
||||
let expected = "\nHello world!";
|
||||
|
||||
assert_eq!(documents.len(), 1);
|
||||
assert_eq!(
|
||||
documents[0].metadata.get("source").unwrap(),
|
||||
&Value::from("https://example.com/")
|
||||
);
|
||||
assert_eq!(documents[0].page_content, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_html_load_from_path() {
|
||||
let buffer = include_bytes!("../../../fixtures/sample.html");
|
||||
let html_loader = HtmlLoader::new(
|
||||
Cursor::new(buffer),
|
||||
Url::parse("https://example.com/").unwrap(),
|
||||
);
|
||||
|
||||
let documents = html_loader.load().unwrap();
|
||||
|
||||
let expected = "Example Domain\n\n This domain is for use in illustrative examples in \
|
||||
documents. You may\n use this domain in literature without prior \
|
||||
coordination or asking for\n permission.\n More information...";
|
||||
|
||||
assert_eq!(documents.len(), 1);
|
||||
assert_eq!(
|
||||
documents[0].metadata.get("source").unwrap(),
|
||||
&Value::from("https://example.com/")
|
||||
);
|
||||
assert_eq!(documents[0].page_content, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
mod docx;
|
||||
mod error;
|
||||
mod html;
|
||||
mod pdf;
|
||||
mod source;
|
||||
mod text;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use super::*;
|
||||
|
||||
// modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
pub trait Loader: Send + Sync {
|
||||
fn load(self) -> Result<Vec<Document>, LoaderError>;
|
||||
fn load_and_split<TS: TextSplitter + 'static>(
|
||||
self,
|
||||
splitter: TS,
|
||||
) -> Result<Vec<Document>, LoaderError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let docs = self.load()?;
|
||||
Ok(splitter.split_documents(&docs)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub use docx::DocxLoader;
|
||||
pub use error::{LoaderError, LoaderResult};
|
||||
pub use html::HtmlLoader;
|
||||
pub use pdf::PdfExtractLoader;
|
||||
pub use source::{get_language_by_filename, LanguageParserOptions, SourceCodeLoader};
|
||||
pub use text::TextLoader;
|
||||
pub use url::Url;
|
||||
@@ -0,0 +1,70 @@
|
||||
use pdf_extract::{output_doc, output_doc_encrypted, PlainTextOutput};
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PdfExtractLoader {
|
||||
document: pdf_extract::Document,
|
||||
}
|
||||
|
||||
impl PdfExtractLoader {
|
||||
pub fn new<R: Read>(reader: R) -> Result<Self, LoaderError> {
|
||||
let document = pdf_extract::Document::load_from(reader)
|
||||
.map_err(|e| LoaderError::OtherError(e.to_string()))?;
|
||||
Ok(Self { document })
|
||||
}
|
||||
}
|
||||
|
||||
impl PdfExtractLoader {
|
||||
fn extract_text(&self) -> Result<String, LoaderError> {
|
||||
let mut doc = self.document.clone();
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let mut output = PlainTextOutput::new(&mut buffer as &mut dyn std::io::Write);
|
||||
if doc.is_encrypted() {
|
||||
output_doc_encrypted(&mut doc, &mut output, "")?;
|
||||
} else {
|
||||
output_doc(&doc, &mut output)?;
|
||||
}
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
}
|
||||
|
||||
fn extract_text_to_doc(&self) -> Result<Document, LoaderError> {
|
||||
let text = self.extract_text()?;
|
||||
Ok(Document::new(text))
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for PdfExtractLoader {
|
||||
fn load(self) -> Result<Vec<Document>, LoaderError> {
|
||||
let doc = self.extract_text_to_doc()?;
|
||||
Ok(vec![doc])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::read, io::Cursor, path::PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_pdf() {
|
||||
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures");
|
||||
let buffer = read(fixtures.join("sample.pdf")).unwrap();
|
||||
|
||||
let reader = Cursor::new(buffer);
|
||||
let loader = PdfExtractLoader::new(reader).expect("Failed to create PdfExtractLoader");
|
||||
|
||||
let docs = loader.load().unwrap();
|
||||
|
||||
assert_eq!(docs.len(), 1);
|
||||
assert_eq!(
|
||||
&docs[0].page_content[..100],
|
||||
"\n\nSample PDF\nThis is a simple PDF file. Fun fun fun.\n\nLorem ipsum dolor sit amet, \
|
||||
consectetuer a"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
mod parser;
|
||||
|
||||
pub use parser::{get_language_by_filename, LanguageParser, LanguageParserOptions};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourceCodeLoader {
|
||||
content: String,
|
||||
parser_option: LanguageParserOptions,
|
||||
}
|
||||
|
||||
impl SourceCodeLoader {
|
||||
pub fn from_string<S: Into<String>>(input: S) -> Self {
|
||||
Self {
|
||||
content: input.into(),
|
||||
parser_option: LanguageParserOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceCodeLoader {
|
||||
pub fn with_parser_option(mut self, parser_option: LanguageParserOptions) -> Self {
|
||||
self.parser_option = parser_option;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for SourceCodeLoader {
|
||||
fn load(self) -> Result<Vec<Document>, LoaderError> {
|
||||
let options = self.parser_option.clone();
|
||||
|
||||
let docs = LanguageParser::from_language(options.language)
|
||||
.with_parser_threshold(options.parser_threshold)
|
||||
.parse_code(&self.content)?;
|
||||
|
||||
Ok(docs)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use parser::Language;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_source_code_loader() {
|
||||
let content = include_str!("../../../../fixtures/sample.rs");
|
||||
let loader = SourceCodeLoader::from_string(content).with_parser_option(LanguageParserOptions {
|
||||
language: Language::Rust,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let documents_with_content = loader.load().unwrap();
|
||||
assert_eq!(documents_with_content.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use std::{collections::HashMap, fmt::Debug, string::ToString};
|
||||
|
||||
use strum_macros::Display;
|
||||
use tree_sitter::{Parser, Tree};
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
#[derive(Display, Debug, Clone)]
|
||||
pub enum Language {
|
||||
Rust,
|
||||
C,
|
||||
Cpp,
|
||||
Javascript,
|
||||
Typescript,
|
||||
Go,
|
||||
Python,
|
||||
}
|
||||
|
||||
pub enum LanguageContentTypes {
|
||||
SimplifiedCode,
|
||||
FunctionsImpls,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LanguageContentTypes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
LanguageContentTypes::SimplifiedCode => "simplified_code",
|
||||
LanguageContentTypes::FunctionsImpls => "functions_impls",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LanguageParserOptions {
|
||||
pub parser_threshold: u64,
|
||||
pub language: Language,
|
||||
}
|
||||
|
||||
impl Default for LanguageParserOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
parser_threshold: 1000,
|
||||
language: Language::Rust,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LanguageParser {
|
||||
parser: Parser,
|
||||
parser_options: LanguageParserOptions,
|
||||
}
|
||||
|
||||
impl Debug for LanguageParser {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"LanguageParser {{ language: {:?} }}",
|
||||
self.parser_options.language
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LanguageParser {
|
||||
fn clone(&self) -> Self {
|
||||
LanguageParser {
|
||||
parser: get_language_parser(&self.parser_options.language),
|
||||
parser_options: self.parser_options.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_language_by_filename(name: &str) -> LoaderResult<Language> {
|
||||
let extension = name
|
||||
.split('.')
|
||||
.last()
|
||||
.ok_or(LoaderError::UnsupportedLanguage)?;
|
||||
let language = match extension.to_lowercase().as_str() {
|
||||
"rs" => Language::Rust,
|
||||
"c" => Language::C,
|
||||
"cpp" => Language::Cpp,
|
||||
"h" => Language::C,
|
||||
"hpp" => Language::Cpp,
|
||||
"js" => Language::Javascript,
|
||||
"ts" => Language::Typescript,
|
||||
"tsx" => Language::Typescript,
|
||||
"go" => Language::Go,
|
||||
"py" => Language::Python,
|
||||
_ => return Err(LoaderError::UnsupportedLanguage),
|
||||
};
|
||||
Ok(language)
|
||||
}
|
||||
|
||||
fn get_language_parser(language: &Language) -> Parser {
|
||||
let mut parser = Parser::new();
|
||||
let lang = match language {
|
||||
Language::Rust => tree_sitter_rust::LANGUAGE,
|
||||
Language::C => tree_sitter_c::LANGUAGE,
|
||||
Language::Cpp => tree_sitter_cpp::LANGUAGE,
|
||||
Language::Javascript => tree_sitter_javascript::LANGUAGE,
|
||||
Language::Typescript => tree_sitter_typescript::LANGUAGE_TSX,
|
||||
Language::Go => tree_sitter_go::LANGUAGE,
|
||||
Language::Python => tree_sitter_python::LANGUAGE,
|
||||
};
|
||||
parser
|
||||
.set_language(&lang.into())
|
||||
.unwrap_or_else(|_| panic!("Error loading grammar for language: {:?}", language));
|
||||
parser
|
||||
}
|
||||
|
||||
impl LanguageParser {
|
||||
pub fn from_language(language: Language) -> Self {
|
||||
Self {
|
||||
parser: get_language_parser(&language),
|
||||
parser_options: LanguageParserOptions {
|
||||
language,
|
||||
..LanguageParserOptions::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_parser_threshold(mut self, threshold: u64) -> Self {
|
||||
self.parser_options.parser_threshold = threshold;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageParser {
|
||||
pub fn parse_code(&mut self, code: &String) -> LoaderResult<Vec<Document>> {
|
||||
let tree = self
|
||||
.parser
|
||||
.parse(code, None)
|
||||
.ok_or(LoaderError::UnsupportedLanguage)?;
|
||||
if self.parser_options.parser_threshold > tree.root_node().end_position().row as u64 {
|
||||
return Ok(vec![Document::new(code).with_metadata(HashMap::from([
|
||||
(
|
||||
"content_type".to_string(),
|
||||
serde_json::Value::from(LanguageContentTypes::SimplifiedCode.to_string()),
|
||||
),
|
||||
(
|
||||
"language".to_string(),
|
||||
serde_json::Value::from(self.parser_options.language.to_string()),
|
||||
),
|
||||
]))]);
|
||||
}
|
||||
self.extract_functions_classes(tree, code)
|
||||
}
|
||||
|
||||
pub fn extract_functions_classes(
|
||||
&self,
|
||||
tree: Tree,
|
||||
code: &String,
|
||||
) -> LoaderResult<Vec<Document>> {
|
||||
let mut chunks = Vec::new();
|
||||
|
||||
let count = tree.root_node().child_count();
|
||||
for i in 0..count {
|
||||
let Some(node) = tree.root_node().child(i) else {
|
||||
continue;
|
||||
};
|
||||
let source_code = node.utf8_text(code.as_bytes())?.to_string();
|
||||
let lang_meta = (
|
||||
"language".to_string(),
|
||||
serde_json::Value::from(self.parser_options.language.to_string()),
|
||||
);
|
||||
if node.kind() == "function_item" || node.kind() == "impl_item" {
|
||||
let doc = Document::new(source_code).with_metadata(HashMap::from([
|
||||
lang_meta.clone(),
|
||||
(
|
||||
"content_type".to_string(),
|
||||
serde_json::Value::from(LanguageContentTypes::FunctionsImpls.to_string()),
|
||||
),
|
||||
]));
|
||||
chunks.push(doc);
|
||||
} else {
|
||||
let doc = Document::new(source_code).with_metadata(HashMap::from([
|
||||
lang_meta.clone(),
|
||||
(
|
||||
"content_type".to_string(),
|
||||
serde_json::Value::from(LanguageContentTypes::SimplifiedCode.to_string()),
|
||||
),
|
||||
]));
|
||||
chunks.push(doc);
|
||||
}
|
||||
}
|
||||
Ok(chunks)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_code_parser() {
|
||||
let code = r#"
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
||||
pub struct Person {
|
||||
name: String,
|
||||
age: i32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
pub fn new(name: String, age: i32) -> Self {
|
||||
Self { name, age }
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn get_age(&self) -> i32 {
|
||||
self.age
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut parser = LanguageParser::from_language(Language::Rust);
|
||||
|
||||
let documents = parser.parse_code(&code.to_string()).unwrap();
|
||||
assert_eq!(documents.len(), 1);
|
||||
|
||||
// Set the parser threshold to 10 for testing
|
||||
let mut parser = parser.with_parser_threshold(10);
|
||||
|
||||
let documents = parser.parse_code(&code.to_string()).unwrap();
|
||||
assert_eq!(documents.len(), 3);
|
||||
assert_eq!(
|
||||
documents[0].page_content,
|
||||
"fn main() {\n println!(\"Hello, world!\");\n }"
|
||||
);
|
||||
assert_eq!(
|
||||
documents[1].metadata.get("content_type").unwrap(),
|
||||
LanguageContentTypes::SimplifiedCode.to_string().as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/document_loaders
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextLoader {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl TextLoader {
|
||||
pub fn new<T: Into<String>>(input: T) -> Self {
|
||||
Self {
|
||||
content: input.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for TextLoader {
|
||||
fn load(self) -> Result<Vec<Document>, LoaderError> {
|
||||
let doc = Document::new(self.content);
|
||||
Ok(vec![doc])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mod document;
|
||||
mod loader;
|
||||
mod splitter;
|
||||
mod types;
|
||||
|
||||
pub use document::{Chunk, Doc};
|
||||
use loader::{
|
||||
get_language_by_filename, DocxLoader, HtmlLoader, LanguageParserOptions, Loader, LoaderError,
|
||||
PdfExtractLoader, SourceCodeLoader, TextLoader, Url,
|
||||
};
|
||||
use splitter::{MarkdownSplitter, TextSplitter, TextSplitterError, TokenSplitter};
|
||||
use types::Document;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/text_splitter
|
||||
*/
|
||||
use text_splitter::ChunkConfigError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TextSplitterError {
|
||||
#[error("Empty input text")]
|
||||
EmptyInputText,
|
||||
|
||||
#[error("Mismatch metadata and text")]
|
||||
MetadataTextMismatch,
|
||||
|
||||
#[error("Tokenizer not found")]
|
||||
TokenizerNotFound,
|
||||
|
||||
#[error("Tokenizer creation failed due to invalid tokenizer")]
|
||||
InvalidTokenizer,
|
||||
|
||||
#[error("Tokenizer creation failed due to invalid model")]
|
||||
InvalidModel,
|
||||
|
||||
#[error("Invalid chunk overlap and size")]
|
||||
InvalidSplitterOptions,
|
||||
|
||||
#[error("Error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
impl From<ChunkConfigError> for TextSplitterError {
|
||||
fn from(_: ChunkConfigError) -> Self {
|
||||
Self::InvalidSplitterOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use text_splitter::ChunkConfig;
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/text_splitter
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
pub struct MarkdownSplitter {
|
||||
splitter_options: SplitterOptions,
|
||||
}
|
||||
|
||||
impl Default for MarkdownSplitter {
|
||||
fn default() -> Self {
|
||||
MarkdownSplitter::new(SplitterOptions::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownSplitter {
|
||||
pub fn new(options: SplitterOptions) -> MarkdownSplitter {
|
||||
MarkdownSplitter {
|
||||
splitter_options: options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextSplitter for MarkdownSplitter {
|
||||
fn split_text(&self, text: &str) -> Result<Vec<String>, TextSplitterError> {
|
||||
let chunk_config = ChunkConfig::try_from(&self.splitter_options)?;
|
||||
Ok(
|
||||
text_splitter::MarkdownSplitter::new(chunk_config)
|
||||
.chunks(text)
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/text_splitter
|
||||
*/
|
||||
mod error;
|
||||
mod markdown;
|
||||
mod options;
|
||||
mod token;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use error::TextSplitterError;
|
||||
pub use markdown::MarkdownSplitter;
|
||||
use options::SplitterOptions;
|
||||
use serde_json::Value;
|
||||
pub use token::TokenSplitter;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub trait TextSplitter: Send + Sync {
|
||||
fn split_text(&self, text: &str) -> Result<Vec<String>, TextSplitterError>;
|
||||
|
||||
fn split_documents(&self, documents: &[Document]) -> Result<Vec<Document>, TextSplitterError> {
|
||||
let mut texts: Vec<String> = Vec::new();
|
||||
let mut metadatas: Vec<HashMap<String, Value>> = Vec::new();
|
||||
documents.iter().for_each(|d| {
|
||||
texts.push(d.page_content.clone());
|
||||
metadatas.push(d.metadata.clone());
|
||||
});
|
||||
|
||||
self.create_documents(&texts, &metadatas)
|
||||
}
|
||||
|
||||
fn create_documents(
|
||||
&self,
|
||||
text: &[String],
|
||||
metadatas: &[HashMap<String, Value>],
|
||||
) -> Result<Vec<Document>, TextSplitterError> {
|
||||
let mut metadatas = metadatas.to_vec();
|
||||
if metadatas.is_empty() {
|
||||
metadatas = vec![HashMap::new(); text.len()];
|
||||
}
|
||||
|
||||
if text.len() != metadatas.len() {
|
||||
return Err(TextSplitterError::MetadataTextMismatch);
|
||||
}
|
||||
|
||||
let mut documents: Vec<Document> = Vec::new();
|
||||
for i in 0..text.len() {
|
||||
let chunks = self.split_text(&text[i])?;
|
||||
for chunk in chunks {
|
||||
let document = Document::new(chunk).with_metadata(metadatas[i].clone());
|
||||
documents.push(document);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(documents)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/text_splitter
|
||||
*/
|
||||
use text_splitter::ChunkConfig;
|
||||
use tiktoken_rs::{get_bpe_from_model, get_bpe_from_tokenizer, tokenizer::Tokenizer, CoreBPE};
|
||||
|
||||
use super::TextSplitterError;
|
||||
|
||||
// Options is a struct that contains options for a text splitter.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplitterOptions {
|
||||
pub chunk_size: usize,
|
||||
pub chunk_overlap: usize,
|
||||
pub model_name: String,
|
||||
pub encoding_name: String,
|
||||
pub trim_chunks: bool,
|
||||
}
|
||||
|
||||
impl Default for SplitterOptions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SplitterOptions {
|
||||
pub fn new() -> Self {
|
||||
SplitterOptions {
|
||||
chunk_size: 512,
|
||||
chunk_overlap: 0,
|
||||
model_name: String::from("gpt-3.5-turbo"),
|
||||
encoding_name: String::from("cl100k_base"),
|
||||
trim_chunks: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Builder pattern for Options struct
|
||||
impl SplitterOptions {
|
||||
pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
|
||||
self.chunk_size = chunk_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chunk_overlap(mut self, chunk_overlap: usize) -> Self {
|
||||
self.chunk_overlap = chunk_overlap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_model_name(mut self, model_name: &str) -> Self {
|
||||
self.model_name = String::from(model_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_encoding_name(mut self, encoding_name: &str) -> Self {
|
||||
self.encoding_name = String::from(encoding_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_trim_chunks(mut self, trim_chunks: bool) -> Self {
|
||||
self.trim_chunks = trim_chunks;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_tokenizer_from_str(s: &str) -> Option<Tokenizer> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"cl100k_base" => Some(Tokenizer::Cl100kBase),
|
||||
"p50k_base" => Some(Tokenizer::P50kBase),
|
||||
"r50k_base" => Some(Tokenizer::R50kBase),
|
||||
"p50k_edit" => Some(Tokenizer::P50kEdit),
|
||||
"gpt2" => Some(Tokenizer::Gpt2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&SplitterOptions> for ChunkConfig<CoreBPE> {
|
||||
type Error = TextSplitterError;
|
||||
|
||||
fn try_from(options: &SplitterOptions) -> Result<Self, Self::Error> {
|
||||
let tk = if !options.encoding_name.is_empty() {
|
||||
let tokenizer = SplitterOptions::get_tokenizer_from_str(&options.encoding_name)
|
||||
.ok_or(TextSplitterError::TokenizerNotFound)?;
|
||||
|
||||
get_bpe_from_tokenizer(tokenizer).map_err(|_| TextSplitterError::InvalidTokenizer)?
|
||||
} else {
|
||||
get_bpe_from_model(&options.model_name).map_err(|_| TextSplitterError::InvalidModel)?
|
||||
};
|
||||
|
||||
Ok(
|
||||
ChunkConfig::new(options.chunk_size)
|
||||
.with_sizer(tk)
|
||||
.with_trim(options.trim_chunks)
|
||||
.with_overlap(options.chunk_overlap)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use text_splitter::ChunkConfig;
|
||||
|
||||
/**
|
||||
* modified from https://github.com/Abraxas-365/langchain-rust/tree/v4.6.0/src/text_splitter
|
||||
*/
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokenSplitter {
|
||||
splitter_options: SplitterOptions,
|
||||
}
|
||||
|
||||
impl Default for TokenSplitter {
|
||||
fn default() -> Self {
|
||||
TokenSplitter::new(SplitterOptions::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenSplitter {
|
||||
pub fn new(options: SplitterOptions) -> TokenSplitter {
|
||||
TokenSplitter {
|
||||
splitter_options: options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextSplitter for TokenSplitter {
|
||||
fn split_text(&self, text: &str) -> Result<Vec<String>, TextSplitterError> {
|
||||
let chunk_config = ChunkConfig::try_from(&self.splitter_options)?;
|
||||
Ok(
|
||||
text_splitter::TextSplitter::new(chunk_config)
|
||||
.chunks(text)
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Document {
|
||||
pub page_content: String,
|
||||
pub metadata: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Constructs a new `Document` with provided `page_content`, an empty
|
||||
/// `metadata` map and a `score` of 0.
|
||||
pub fn new<S: Into<String>>(page_content: S) -> Self {
|
||||
Document {
|
||||
page_content: page_content.into(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `metadata` Map of the `Document` to the provided HashMap.
|
||||
pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Document {
|
||||
/// Provides a default `Document` with an empty `page_content`, an empty
|
||||
/// `metadata` map and a `score` of 0.
|
||||
fn default() -> Self {
|
||||
Document {
|
||||
page_content: "".to_string(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
#[cfg(feature = "doc-loader")]
|
||||
pub mod doc_loader;
|
||||
pub mod hashcash;
|
||||
|
||||
@@ -15,8 +15,12 @@ export class BlobFrontend {
|
||||
return this.sync.uploadBlob(blob);
|
||||
}
|
||||
|
||||
fullSync() {
|
||||
return this.sync.fullSync();
|
||||
fullDownload() {
|
||||
return this.sync.fullDownload();
|
||||
}
|
||||
|
||||
fullUpload() {
|
||||
return this.sync.fullUpload();
|
||||
}
|
||||
|
||||
addPriority(_id: string, _priority: number) {
|
||||
|
||||
@@ -399,31 +399,23 @@ export class DocFrontend {
|
||||
this.statusUpdatedSubject$.next(job.docId);
|
||||
}
|
||||
|
||||
/**
|
||||
* skip listen doc update when apply update
|
||||
*/
|
||||
private skipDocUpdate = false;
|
||||
|
||||
applyUpdate(docId: string, update: Uint8Array) {
|
||||
const doc = this.status.docs.get(docId);
|
||||
if (doc && !isEmptyUpdate(update)) {
|
||||
try {
|
||||
this.skipDocUpdate = true;
|
||||
applyUpdate(doc, update, NBSTORE_ORIGIN);
|
||||
} catch (err) {
|
||||
console.error('failed to apply update yjs doc', err);
|
||||
} finally {
|
||||
this.skipDocUpdate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleDocUpdate = (
|
||||
update: Uint8Array,
|
||||
_origin: any,
|
||||
origin: any,
|
||||
doc: YDoc
|
||||
) => {
|
||||
if (this.skipDocUpdate) {
|
||||
if (origin === NBSTORE_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
if (!this.status.docs.has(doc.guid)) {
|
||||
@@ -534,4 +526,8 @@ export class DocFrontend {
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
async resetSync() {
|
||||
await this.sync.resetSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { PeerStorageOptions } from '../types';
|
||||
|
||||
export interface BlobSyncState {
|
||||
isStorageOverCapacity: boolean;
|
||||
total: number;
|
||||
synced: number;
|
||||
}
|
||||
|
||||
export interface BlobSync {
|
||||
@@ -18,7 +20,8 @@ export interface BlobSync {
|
||||
signal?: AbortSignal
|
||||
): Promise<BlobRecord | null>;
|
||||
uploadBlob(blob: BlobRecord, signal?: AbortSignal): Promise<void>;
|
||||
fullSync(signal?: AbortSignal): Promise<void>;
|
||||
fullDownload(signal?: AbortSignal): Promise<void>;
|
||||
fullUpload(signal?: AbortSignal): Promise<void>;
|
||||
setMaxBlobSize(size: number): void;
|
||||
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void;
|
||||
}
|
||||
@@ -26,6 +29,8 @@ export interface BlobSync {
|
||||
export class BlobSyncImpl implements BlobSync {
|
||||
readonly state$ = new BehaviorSubject<BlobSyncState>({
|
||||
isStorageOverCapacity: false,
|
||||
total: Object.values(this.storages.remotes).length ? 1 : 0,
|
||||
synced: 0,
|
||||
});
|
||||
private abort: AbortController | null = null;
|
||||
private maxBlobSize: number = 1024 * 1024 * 100; // 100MB
|
||||
@@ -34,19 +39,24 @@ export class BlobSyncImpl implements BlobSync {
|
||||
constructor(readonly storages: PeerStorageOptions<BlobStorage>) {}
|
||||
|
||||
async downloadBlob(blobId: string, signal?: AbortSignal) {
|
||||
const localBlob = await this.storages.local.get(blobId, signal);
|
||||
if (localBlob) {
|
||||
return localBlob;
|
||||
}
|
||||
|
||||
for (const storage of Object.values(this.storages.remotes)) {
|
||||
const data = await storage.get(blobId, signal);
|
||||
if (data) {
|
||||
await this.storages.local.set(data, signal);
|
||||
return data;
|
||||
try {
|
||||
const localBlob = await this.storages.local.get(blobId, signal);
|
||||
if (localBlob) {
|
||||
return localBlob;
|
||||
}
|
||||
|
||||
for (const storage of Object.values(this.storages.remotes)) {
|
||||
const data = await storage.get(blobId, signal);
|
||||
if (data) {
|
||||
await this.storages.local.set(data, signal);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error('error when download blob', e);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async uploadBlob(blob: BlobRecord, signal?: AbortSignal) {
|
||||
@@ -62,7 +72,11 @@ export class BlobSyncImpl implements BlobSync {
|
||||
return await remote.set(blob, signal);
|
||||
} catch (err) {
|
||||
if (err instanceof OverCapacityError) {
|
||||
this.state$.next({ isStorageOverCapacity: true });
|
||||
this.state$.next({
|
||||
isStorageOverCapacity: true,
|
||||
total: this.state$.value.total,
|
||||
synced: this.state$.value.synced,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@@ -70,71 +84,95 @@ export class BlobSyncImpl implements BlobSync {
|
||||
);
|
||||
}
|
||||
|
||||
async fullSync(signal?: AbortSignal) {
|
||||
async fullDownload(signal?: AbortSignal) {
|
||||
throwIfAborted(signal);
|
||||
|
||||
await this.storages.local.connection.waitForConnected(signal);
|
||||
const localList = (await this.storages.local.list(signal)).map(b => b.key);
|
||||
this.state$.next({
|
||||
...this.state$.value,
|
||||
synced: localList.length,
|
||||
});
|
||||
|
||||
for (const [remotePeer, remote] of Object.entries(this.storages.remotes)) {
|
||||
let localList: string[] = [];
|
||||
let remoteList: string[] = [];
|
||||
await Promise.allSettled(
|
||||
Object.entries(this.storages.remotes).map(
|
||||
async ([remotePeer, remote]) => {
|
||||
await remote.connection.waitForConnected(signal);
|
||||
|
||||
await remote.connection.waitForConnected(signal);
|
||||
const remoteList = (await remote.list(signal)).map(b => b.key);
|
||||
|
||||
try {
|
||||
localList = (await this.storages.local.list(signal)).map(b => b.key);
|
||||
throwIfAborted(signal);
|
||||
remoteList = (await remote.list(signal)).map(b => b.key);
|
||||
throwIfAborted(signal);
|
||||
} catch (err) {
|
||||
if (err === MANUALLY_STOP) {
|
||||
throw err;
|
||||
}
|
||||
console.error(`error when sync`, err);
|
||||
continue;
|
||||
}
|
||||
this.state$.next({
|
||||
...this.state$.value,
|
||||
total: Math.max(this.state$.value.total, remoteList.length),
|
||||
});
|
||||
|
||||
const needUpload = difference(localList, remoteList);
|
||||
for (const key of needUpload) {
|
||||
try {
|
||||
const data = await this.storages.local.get(key, signal);
|
||||
throwIfAborted(signal);
|
||||
if (data) {
|
||||
await remote.set(data, signal);
|
||||
throwIfAborted(signal);
|
||||
|
||||
const needDownload = difference(remoteList, localList);
|
||||
for (const key of needDownload) {
|
||||
try {
|
||||
const data = await remote.get(key, signal);
|
||||
throwIfAborted(signal);
|
||||
if (data) {
|
||||
await this.storages.local.set(data, signal);
|
||||
this.state$.next({
|
||||
...this.state$.value,
|
||||
synced: this.state$.value.synced + 1,
|
||||
});
|
||||
throwIfAborted(signal);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === MANUALLY_STOP) {
|
||||
throw err;
|
||||
}
|
||||
console.error(
|
||||
`error when sync ${key} from [${remotePeer}] to [local]`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === MANUALLY_STOP) {
|
||||
throw err;
|
||||
}
|
||||
console.error(
|
||||
`error when sync ${key} from [local] to [${remotePeer}]`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const needDownload = difference(remoteList, localList);
|
||||
async fullUpload(signal?: AbortSignal) {
|
||||
throwIfAborted(signal);
|
||||
|
||||
await this.storages.local.connection.waitForConnected(signal);
|
||||
const localList = (await this.storages.local.list(signal)).map(b => b.key);
|
||||
|
||||
await Promise.allSettled(
|
||||
Object.entries(this.storages.remotes).map(
|
||||
async ([remotePeer, remote]) => {
|
||||
await remote.connection.waitForConnected(signal);
|
||||
|
||||
const remoteList = (await remote.list(signal)).map(b => b.key);
|
||||
|
||||
for (const key of needDownload) {
|
||||
try {
|
||||
const data = await remote.get(key, signal);
|
||||
throwIfAborted(signal);
|
||||
if (data) {
|
||||
await this.storages.local.set(data, signal);
|
||||
throwIfAborted(signal);
|
||||
|
||||
const needUpload = difference(localList, remoteList);
|
||||
for (const key of needUpload) {
|
||||
try {
|
||||
const data = await this.storages.local.get(key, signal);
|
||||
throwIfAborted(signal);
|
||||
if (data) {
|
||||
await remote.set(data, signal);
|
||||
throwIfAborted(signal);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === MANUALLY_STOP) {
|
||||
throw err;
|
||||
}
|
||||
console.error(
|
||||
`error when sync ${key} from [local] to [${remotePeer}]`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === MANUALLY_STOP) {
|
||||
throw err;
|
||||
}
|
||||
console.error(
|
||||
`error when sync ${key} from [${remotePeer}] to [local]`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -144,16 +182,12 @@ export class BlobSyncImpl implements BlobSync {
|
||||
|
||||
const abort = new AbortController();
|
||||
this.abort = abort;
|
||||
|
||||
// TODO(@eyhn): fix this, large blob may cause iOS to crash?
|
||||
if (!BUILD_CONFIG.isIOS) {
|
||||
this.fullSync(abort.signal).catch(error => {
|
||||
if (error === MANUALLY_STOP) {
|
||||
return;
|
||||
}
|
||||
console.error('sync blob error', error);
|
||||
});
|
||||
}
|
||||
this.fullUpload(abort.signal).catch(error => {
|
||||
if (error === MANUALLY_STOP) {
|
||||
return;
|
||||
}
|
||||
console.error('sync blob error', error);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DocSync {
|
||||
readonly state$: Observable<DocSyncState>;
|
||||
docState$(docId: string): Observable<DocSyncDocState>;
|
||||
addPriority(id: string, priority: number): () => void;
|
||||
resetSync(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DocSyncImpl implements DocSync {
|
||||
@@ -127,4 +128,13 @@ export class DocSyncImpl implements DocSync {
|
||||
const undo = this.peers.map(peer => peer.addPriority(id, priority));
|
||||
return () => undo.forEach(fn => fn());
|
||||
}
|
||||
|
||||
async resetSync() {
|
||||
const running = this.abort !== null;
|
||||
this.stop();
|
||||
await this.sync.clearClocks();
|
||||
if (running) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ export class DocSyncPeer {
|
||||
jobs: (Job & { type: 'push' })[],
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
if (this.status.connectedDocs.has(docId)) {
|
||||
if (this.status.connectedDocs.has(docId) && !this.remote.isReadonly) {
|
||||
const maxClock = jobs.reduce(
|
||||
(a, b) => (a.getTime() > b.clock.getTime() ? a : b.clock),
|
||||
new Date(0)
|
||||
@@ -680,6 +680,7 @@ export class DocSyncPeer {
|
||||
const cachedClocks = await this.syncMetadata.getPeerRemoteClocks(
|
||||
this.peerId
|
||||
);
|
||||
this.status.remoteClocks.clear();
|
||||
throwIfAborted(signal);
|
||||
for (const [id, v] of Object.entries(cachedClocks)) {
|
||||
this.status.remoteClocks.set(id, v);
|
||||
@@ -690,8 +691,9 @@ export class DocSyncPeer {
|
||||
const maxClockValue = this.status.remoteClocks.max;
|
||||
const newClocks = await this.remote.getDocTimestamps(maxClockValue);
|
||||
for (const [id, v] of Object.entries(newClocks)) {
|
||||
this.actions.updateRemoteClock(id, v);
|
||||
this.status.remoteClocks.set(id, v);
|
||||
}
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
|
||||
for (const [id, v] of Object.entries(newClocks)) {
|
||||
await this.syncMetadata.setPeerRemoteClock(this.peerId, {
|
||||
|
||||
@@ -222,6 +222,10 @@ class WorkerDocSync implements DocSync {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
resetSync(): Promise<void> {
|
||||
return this.client.call('docSync.resetSync');
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerBlobSync implements BlobSync {
|
||||
@@ -253,26 +257,23 @@ class WorkerBlobSync implements BlobSync {
|
||||
uploadBlob(blob: BlobRecord, _signal?: AbortSignal): Promise<void> {
|
||||
return this.client.call('blobSync.uploadBlob', blob);
|
||||
}
|
||||
fullSync(signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const abortListener = () => {
|
||||
reject(signal?.reason);
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
fullDownload(signal?: AbortSignal): Promise<void> {
|
||||
const download = this.client.call('blobSync.fullDownload');
|
||||
|
||||
signal?.addEventListener('abort', abortListener);
|
||||
|
||||
const subscription = this.client.ob$('blobSync.fullSync').subscribe({
|
||||
next() {
|
||||
signal?.removeEventListener('abort', abortListener);
|
||||
resolve();
|
||||
},
|
||||
error(err) {
|
||||
signal?.removeEventListener('abort', abortListener);
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
signal?.addEventListener('abort', () => {
|
||||
download.cancel();
|
||||
});
|
||||
|
||||
return download;
|
||||
}
|
||||
fullUpload(signal?: AbortSignal): Promise<void> {
|
||||
const upload = this.client.call('blobSync.fullUpload');
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
upload.cancel();
|
||||
});
|
||||
|
||||
return upload;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,22 +231,13 @@ class StoreConsumer {
|
||||
const undo = this.docSync.addPriority(docId, priority);
|
||||
return () => undo();
|
||||
}),
|
||||
'docSync.resetSync': () => this.docSync.resetSync(),
|
||||
'blobSync.downloadBlob': key => this.blobSync.downloadBlob(key),
|
||||
'blobSync.uploadBlob': blob => this.blobSync.uploadBlob(blob),
|
||||
'blobSync.fullSync': () =>
|
||||
new Observable(subscriber => {
|
||||
const abortController = new AbortController();
|
||||
this.blobSync
|
||||
.fullSync(abortController.signal)
|
||||
.then(() => {
|
||||
subscriber.next(true);
|
||||
subscriber.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subscriber.error(error);
|
||||
});
|
||||
return () => abortController.abort(MANUALLY_STOP);
|
||||
}),
|
||||
'blobSync.fullDownload': (_, { signal }) =>
|
||||
this.blobSync.fullDownload(signal),
|
||||
'blobSync.fullUpload': (_, { signal }) =>
|
||||
this.blobSync.fullUpload(signal),
|
||||
'blobSync.state': () => this.blobSync.state$,
|
||||
'blobSync.setMaxBlobSize': size => this.blobSync.setMaxBlobSize(size),
|
||||
'blobSync.onReachedMaxBlobSize': () =>
|
||||
|
||||
@@ -81,12 +81,14 @@ interface GroupedWorkerOps {
|
||||
state: [void, DocSyncState];
|
||||
docState: [string, DocSyncDocState];
|
||||
addPriority: [{ docId: string; priority: number }, boolean];
|
||||
resetSync: [void, void];
|
||||
};
|
||||
|
||||
blobSync: {
|
||||
downloadBlob: [string, BlobRecord | null];
|
||||
uploadBlob: [BlobRecord, void];
|
||||
fullSync: [void, boolean];
|
||||
fullDownload: [void, void];
|
||||
fullUpload: [void, void];
|
||||
setMaxBlobSize: [number, void];
|
||||
onReachedMaxBlobSize: [void, number];
|
||||
state: [void, BlobSyncState];
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { createComponent as createReactComponentFromLit } from './create-component';
|
||||
export {
|
||||
createComponent as createReactComponentFromLit,
|
||||
type ReactWebComponent,
|
||||
} from './create-component';
|
||||
export * from './lit-portal';
|
||||
export { toReactNode } from './to-react-node';
|
||||
export { templateToString } from './utils';
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@dotlottie/player-component": "^2.7.12",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@floating-ui/dom": "^1.6.12",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createReactComponentFromLit } from '@affine/component';
|
||||
import {
|
||||
BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
@@ -30,6 +31,7 @@ import { property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import React from 'react';
|
||||
|
||||
import { markDownToDoc } from '../../utils';
|
||||
import type {
|
||||
@@ -349,12 +351,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
accessor state: AffineAIPanelState | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'text-renderer': TextRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export const createTextRenderer: (
|
||||
host: EditorHost,
|
||||
options: TextRendererOptions
|
||||
@@ -368,3 +364,14 @@ export const createTextRenderer: (
|
||||
></text-renderer>`;
|
||||
};
|
||||
};
|
||||
|
||||
export const LitTextRenderer = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: TextRenderer,
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'text-renderer': TextRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
CodeBlockSpec,
|
||||
EdgelessRootBlockSpec,
|
||||
@@ -19,6 +20,12 @@ export function enableAIExtension(
|
||||
specBuilder: SpecBuilder,
|
||||
framework: FrameworkProvider
|
||||
) {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
if (!enableAI) {
|
||||
return;
|
||||
}
|
||||
|
||||
specBuilder.replace(CodeBlockSpec, AICodeBlockSpec);
|
||||
specBuilder.replace(ImageBlockSpec, AIImageBlockSpec);
|
||||
specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec);
|
||||
|
||||
+8
-18
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
createReactComponentFromLit,
|
||||
Divider,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { Button, Divider, useLitPortalFactory } from '@affine/component';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import {
|
||||
type Backlink,
|
||||
@@ -28,7 +23,7 @@ import {
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
import {
|
||||
Fragment,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -40,18 +35,13 @@ import {
|
||||
AffinePageReference,
|
||||
AffineSharedPageReference,
|
||||
} from '../../components/affine/reference-link';
|
||||
import { TextRenderer } from '../ai/components/text-renderer';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
import { LitTextRenderer } from '../ai/components/text-renderer';
|
||||
import { enableEditorExtension } from '../extensions/entry/enable-editor';
|
||||
import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
|
||||
const BlocksuiteTextRenderer = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: TextRenderer,
|
||||
});
|
||||
} from '../extensions/reference-renderer';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
|
||||
const PREFIX = 'bi-directional-link-panel-collapse:';
|
||||
|
||||
@@ -154,7 +144,7 @@ const usePreviewExtensions = () => {
|
||||
}, [workspaceService]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const specs = createPageModeSpecs(framework);
|
||||
const specs = enableEditorExtension(framework, 'page');
|
||||
specs.extend([patchReferenceRenderer(reactToLit, referenceRenderer)]);
|
||||
return specs.value;
|
||||
}, [reactToLit, referenceRenderer, framework]);
|
||||
@@ -288,7 +278,7 @@ export const BacklinkGroups = () => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<BlocksuiteTextRenderer
|
||||
<LitTextRenderer
|
||||
className={styles.linkPreviewRenderer}
|
||||
answer={link.markdownPreview}
|
||||
schema={getAFFiNEWorkspaceSchema()}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { registerAIEffects } from '@affine/core/blocksuite/ai/effects';
|
||||
import { effects as editorEffects } from '@affine/core/blocksuite/editors';
|
||||
import { editorEffects } from '@affine/core/blocksuite/editors';
|
||||
import { effects as bsEffects } from '@blocksuite/affine/effects';
|
||||
|
||||
import { effects as edgelessEffects } from './specs/edgeless';
|
||||
import { effects as patchEffects } from './specs/preview';
|
||||
import { registerTemplates } from './register-templates';
|
||||
|
||||
bsEffects();
|
||||
patchEffects();
|
||||
editorEffects();
|
||||
edgelessEffects();
|
||||
registerAIEffects();
|
||||
registerTemplates();
|
||||
|
||||
export * from './blocksuite-editor';
|
||||
export * from './blocksuite-editor-container';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useConfirmModal, useLitPortalFactory } from '@affine/component';
|
||||
import {
|
||||
createReactComponentFromLit,
|
||||
useConfirmModal,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { EdgelessEditor, PageEditor } from '@affine/core/blocksuite/editors';
|
||||
type EdgelessEditor,
|
||||
LitDocEditor,
|
||||
LitDocTitle,
|
||||
LitEdgelessEditor,
|
||||
type PageEditor,
|
||||
} from '@affine/core/blocksuite/editors';
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type {
|
||||
@@ -18,16 +20,7 @@ import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import track from '@affine/track';
|
||||
import {
|
||||
codeToolbarWidget,
|
||||
type DocMode,
|
||||
DocTitle,
|
||||
embedCardToolbarWidget,
|
||||
formatBarWidget,
|
||||
imageToolbarWidget,
|
||||
slashMenuWidget,
|
||||
surfaceRefToolbarWidget,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { DocMode, DocTitle } from '@blocksuite/affine/blocks';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
useFramework,
|
||||
@@ -35,7 +28,8 @@ import {
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
import type React from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
@@ -52,46 +46,29 @@ import {
|
||||
type DefaultOpenProperty,
|
||||
DocPropertiesTable,
|
||||
} from '../../components/doc-properties';
|
||||
import { patchForAttachmentEmbedViews } from '../extensions/attachment-embed-view';
|
||||
import { patchDocModeService } from '../extensions/doc-mode-service';
|
||||
import { patchDocUrlExtensions } from '../extensions/doc-url';
|
||||
import { EdgelessClipboardWatcher } from '../extensions/edgeless-clipboard';
|
||||
import { patchForClipboardInElectron } from '../extensions/electron-clipboard';
|
||||
import { enableEditorExtension } from '../extensions/entry/enable-editor';
|
||||
import { enableMobileExtension } from '../extensions/entry/enable-mobile';
|
||||
import { enablePreviewExtension } from '../extensions/entry/enable-preview';
|
||||
import { patchForEdgelessNoteConfig } from '../extensions/note-config';
|
||||
import { patchNotificationService } from '../extensions/notification-service';
|
||||
import { patchOpenDocExtension } from '../extensions/open-doc';
|
||||
import { patchPeekViewService } from '../extensions/peek-view-service';
|
||||
import { patchQuickSearchService } from '../extensions/quick-search-service';
|
||||
import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from '../extensions/reference-renderer';
|
||||
import { patchSideBarService } from '../extensions/side-bar-service';
|
||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||
import { extendEdgelessPreviewSpec } from './specs/custom/root-block';
|
||||
import {
|
||||
patchDocModeService,
|
||||
patchEdgelessClipboard,
|
||||
patchForAttachmentEmbedViews,
|
||||
patchForClipboardInElectron,
|
||||
patchForEdgelessNoteConfig,
|
||||
patchForMobile,
|
||||
patchGenerateDocUrlExtension,
|
||||
patchNotificationService,
|
||||
patchOpenDocExtension,
|
||||
patchParseDocUrlExtension,
|
||||
patchPeekViewService,
|
||||
patchQuickSearchService,
|
||||
patchReferenceRenderer,
|
||||
patchSideBarService,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createEdgelessModeSpecs } from './specs/edgeless';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
import { StarterBar } from './starter-bar';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const adapted = {
|
||||
DocEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: PageEditor,
|
||||
}),
|
||||
DocTitle: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: DocTitle,
|
||||
}),
|
||||
EdgelessEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: EdgelessEditor,
|
||||
}),
|
||||
};
|
||||
|
||||
interface BlocksuiteEditorProps {
|
||||
page: Store;
|
||||
readonly?: boolean;
|
||||
@@ -147,16 +124,13 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
}, [workspaceService]);
|
||||
|
||||
useMemo(() => {
|
||||
extendEdgelessPreviewSpec(framework);
|
||||
enablePreviewExtension(framework);
|
||||
}, [framework]);
|
||||
|
||||
const confirmModal = useConfirmModal();
|
||||
|
||||
const patchedSpecs = useMemo(() => {
|
||||
const builder =
|
||||
mode === 'edgeless'
|
||||
? createEdgelessModeSpecs(framework)
|
||||
: createPageModeSpecs(framework);
|
||||
const builder = enableEditorExtension(framework, mode);
|
||||
|
||||
builder.extend(
|
||||
[
|
||||
@@ -165,9 +139,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
patchNotificationService(confirmModal),
|
||||
patchPeekViewService(peekViewService),
|
||||
patchOpenDocExtension(),
|
||||
patchEdgelessClipboard(),
|
||||
patchParseDocUrlExtension(framework),
|
||||
patchGenerateDocUrlExtension(framework),
|
||||
EdgelessClipboardWatcher,
|
||||
patchDocUrlExtensions(framework),
|
||||
patchQuickSearchService(framework),
|
||||
patchSideBarService(framework),
|
||||
patchDocModeService(docService, docsService, editorService),
|
||||
@@ -178,13 +151,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
builder.extend([patchForAttachmentEmbedViews(reactToLit)]);
|
||||
}
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
builder.omit(formatBarWidget);
|
||||
builder.omit(embedCardToolbarWidget);
|
||||
builder.omit(slashMenuWidget);
|
||||
builder.omit(codeToolbarWidget);
|
||||
builder.omit(imageToolbarWidget);
|
||||
builder.omit(surfaceRefToolbarWidget);
|
||||
builder.extend([patchForMobile()].flat());
|
||||
enableMobileExtension(builder);
|
||||
}
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
builder.extend([patchForClipboardInElectron(framework)].flat());
|
||||
@@ -264,7 +231,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
if (typeof externalTitleRef === 'function') {
|
||||
externalTitleRef(el);
|
||||
} else {
|
||||
(externalTitleRef as any).current = el;
|
||||
externalTitleRef.current = el;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -319,7 +286,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
<>
|
||||
<div className={styles.affineDocViewport}>
|
||||
{!isJournal ? (
|
||||
<adapted.DocTitle doc={page} ref={onTitleRef} />
|
||||
<LitDocTitle doc={page} ref={onTitleRef} />
|
||||
) : (
|
||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||
)}
|
||||
@@ -335,7 +302,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<adapted.DocEditor
|
||||
<LitDocEditor
|
||||
className={styles.docContainer}
|
||||
ref={onDocRef}
|
||||
doc={page}
|
||||
@@ -391,7 +358,7 @@ export const BlocksuiteEdgelessEditor = forwardRef<
|
||||
|
||||
return (
|
||||
<div className={styles.affineEdgelessDocViewport}>
|
||||
<adapted.EdgelessEditor ref={onDocRef} doc={page} specs={specs} />
|
||||
<LitEdgelessEditor ref={onDocRef} doc={page} specs={specs} />
|
||||
{portals}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless';
|
||||
import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers';
|
||||
import {
|
||||
EdgelessTemplatePanel,
|
||||
type TemplateManager,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
|
||||
export function registerTemplates() {
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInStickersTemplates as TemplateManager
|
||||
);
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInEdgelessTemplates as TemplateManager
|
||||
);
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
DocDisplayMetaExtension,
|
||||
DocDisplayMetaParams,
|
||||
Signal,
|
||||
SpecBuilder,
|
||||
TelemetryEventMap,
|
||||
ThemeExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
ColorScheme,
|
||||
createSignalFromObservable,
|
||||
DatabaseConfigExtension,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingExtension,
|
||||
referenceToNode,
|
||||
RootBlockConfigExtension,
|
||||
SpecProvider,
|
||||
TelemetryProvider,
|
||||
ThemeExtensionIdentifier,
|
||||
ToolbarMoreMenuConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
import { getFontConfigExtension } from '../font-extension';
|
||||
import { createDatabaseOptionsConfig } from './database-block';
|
||||
import { createLinkedWidgetConfig } from './widgets/linked';
|
||||
import { createToolbarMoreMenuConfig } from './widgets/toolbar';
|
||||
|
||||
function getTelemetryExtension(): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(TelemetryProvider, () => ({
|
||||
track: <T extends keyof TelemetryEventMap>(
|
||||
eventName: T,
|
||||
props: TelemetryEventMap[T]
|
||||
) => {
|
||||
mixpanel.track(eventName as string, props as Record<string, unknown>);
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getThemeExtension(framework: FrameworkProvider) {
|
||||
class AffineThemeExtension
|
||||
extends LifeCycleWatcher
|
||||
implements ThemeExtension
|
||||
{
|
||||
static override readonly key = 'affine-theme';
|
||||
|
||||
private readonly themes: Map<string, Signal<ColorScheme>> = new Map();
|
||||
|
||||
protected readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(ThemeExtensionIdentifier, AffineThemeExtension, [
|
||||
StdIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
getAppTheme() {
|
||||
const keyName = 'app-theme';
|
||||
const cache = this.themes.get(keyName);
|
||||
if (cache) return cache;
|
||||
|
||||
const theme$: Observable<ColorScheme> = framework
|
||||
.get(AppThemeService)
|
||||
.appTheme.theme$.map(theme => {
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
});
|
||||
const { signal: themeSignal, cleanup } =
|
||||
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
|
||||
this.disposables.push(cleanup);
|
||||
this.themes.set(keyName, themeSignal);
|
||||
return themeSignal;
|
||||
}
|
||||
|
||||
getEdgelessTheme(docId?: string) {
|
||||
const doc =
|
||||
(docId && framework.get(DocsService).list.doc$(docId).getValue()) ||
|
||||
framework.get(DocService).doc;
|
||||
|
||||
const cache = this.themes.get(doc.id);
|
||||
if (cache) return cache;
|
||||
|
||||
const appTheme$ = framework.get(AppThemeService).appTheme.theme$;
|
||||
const docTheme$ = doc.properties$.map(
|
||||
props => props.edgelessColorTheme || 'system'
|
||||
);
|
||||
const theme$: Observable<ColorScheme> = combineLatest([
|
||||
appTheme$,
|
||||
docTheme$,
|
||||
]).pipe(
|
||||
map(([appTheme, docTheme]) => {
|
||||
const theme = docTheme === 'system' ? appTheme : docTheme;
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
})
|
||||
);
|
||||
const { signal: themeSignal, cleanup } =
|
||||
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
|
||||
this.disposables.push(cleanup);
|
||||
this.themes.set(doc.id, themeSignal);
|
||||
return themeSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
}
|
||||
}
|
||||
|
||||
return AffineThemeExtension;
|
||||
}
|
||||
|
||||
export function buildDocDisplayMetaExtension(framework: FrameworkProvider) {
|
||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||
|
||||
function iconBuilder(
|
||||
icon: typeof PageIcon,
|
||||
size = '1.25em',
|
||||
style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;'
|
||||
) {
|
||||
return icon({
|
||||
width: size,
|
||||
height: size,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
class AffineDocDisplayMetaService
|
||||
extends LifeCycleWatcher
|
||||
implements DocDisplayMetaExtension
|
||||
{
|
||||
static override key = 'doc-display-meta';
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(DocDisplayMetaProvider, this, [StdIdentifier]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
this.disposables.pop()?.();
|
||||
}
|
||||
}
|
||||
|
||||
icon(
|
||||
docId: string,
|
||||
{ params, title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<TemplateResult> {
|
||||
const icon$ = docDisplayMetaService
|
||||
.icon$(docId, {
|
||||
type: 'lit',
|
||||
title,
|
||||
reference: referenced,
|
||||
referenceToNode: referenceToNode({ pageId: docId, params }),
|
||||
})
|
||||
.map(iconBuilder);
|
||||
|
||||
const { signal: iconSignal, cleanup } = createSignalFromObservable(
|
||||
icon$,
|
||||
iconBuilder(referenced ? LinkedPageIcon : PageIcon)
|
||||
);
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return iconSignal;
|
||||
}
|
||||
|
||||
title(
|
||||
docId: string,
|
||||
{ title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<string> {
|
||||
const title$ = docDisplayMetaService.title$(docId, {
|
||||
title,
|
||||
reference: referenced,
|
||||
});
|
||||
|
||||
const { signal: titleSignal, cleanup } =
|
||||
createSignalFromObservable<string>(title$, title ?? '');
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return titleSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return AffineDocDisplayMetaService;
|
||||
}
|
||||
|
||||
function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
return [
|
||||
EditorSettingExtension(editorSettingService.editorSetting.settingSignal),
|
||||
DatabaseConfigExtension(createDatabaseOptionsConfig(framework)),
|
||||
RootBlockConfigExtension({
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
];
|
||||
}
|
||||
|
||||
export const extendEdgelessPreviewSpec = (function () {
|
||||
let _extension: ExtensionType;
|
||||
let _framework: FrameworkProvider;
|
||||
return function (framework: FrameworkProvider) {
|
||||
if (framework === _framework && _extension) {
|
||||
return _extension;
|
||||
} else {
|
||||
_extension && SpecProvider._.omitSpec('preview:edgeless', _extension);
|
||||
_extension = getThemeExtension(framework);
|
||||
_framework = framework;
|
||||
SpecProvider._.extendSpec('preview:edgeless', [_extension]);
|
||||
return _extension;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
export function enableAffineExtension(
|
||||
framework: FrameworkProvider,
|
||||
specBuilder: SpecBuilder
|
||||
): void {
|
||||
specBuilder.extend(
|
||||
[
|
||||
getThemeExtension(framework),
|
||||
getFontConfigExtension(),
|
||||
getTelemetryExtension(),
|
||||
getEditorConfigExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
].flat()
|
||||
);
|
||||
}
|
||||
-761
@@ -1,761 +0,0 @@
|
||||
import {
|
||||
type ElementOrFactory,
|
||||
Input,
|
||||
notify,
|
||||
toast,
|
||||
type ToastOptions,
|
||||
toReactNode,
|
||||
type useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { type DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import {
|
||||
CreationQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
LinksQuickSearchSession,
|
||||
QuickSearchService,
|
||||
RecentDocsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links';
|
||||
import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
BlockServiceWatcher,
|
||||
type BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
ConfigIdentifier,
|
||||
LifeCycleWatcher,
|
||||
type WidgetComponent,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
AffineReference,
|
||||
CodeBlockConfig,
|
||||
DocMode,
|
||||
DocModeProvider,
|
||||
OpenDocConfig,
|
||||
OpenDocConfigItem,
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
QuickSearchResult,
|
||||
ReferenceNodeConfig,
|
||||
RootBlockConfig,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
AffineSlashMenuWidget,
|
||||
AttachmentEmbedConfigIdentifier,
|
||||
DocModeExtension,
|
||||
EdgelessRootBlockComponent,
|
||||
EmbedLinkedDocBlockComponent,
|
||||
FeatureFlagService,
|
||||
GenerateDocUrlExtension,
|
||||
insertLinkByQuickSearchCommand,
|
||||
NativeClipboardExtension,
|
||||
NoteConfigExtension,
|
||||
NotificationExtension,
|
||||
OpenDocExtension,
|
||||
ParagraphBlockService,
|
||||
ParseDocUrlExtension,
|
||||
PeekViewExtension,
|
||||
QuickSearchExtension,
|
||||
ReferenceNodeConfigExtension,
|
||||
ReferenceNodeConfigIdentifier,
|
||||
RootBlockConfigExtension,
|
||||
SidebarExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
type BlockSnapshot,
|
||||
type ExtensionType,
|
||||
Text,
|
||||
} from '@blocksuite/affine/store';
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import {
|
||||
CenterPeekIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { AttachmentEmbedPreview } from '../../../../components/attachment-viewer/pdf-viewer-embedded';
|
||||
import { generateUrl } from '../../../../components/hooks/affine/use-share-url';
|
||||
import type { DocProps } from '../../../initialization';
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../journal-doc-title';
|
||||
import { EdgelessNoteHeader } from './widgets/edgeless-note-header';
|
||||
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
||||
|
||||
export type ReferenceReactRenderer = (
|
||||
reference: AffineReference
|
||||
) => React.ReactElement;
|
||||
|
||||
const logger = new DebugLogger('affine::spec-patchers');
|
||||
|
||||
function patchSpecService(
|
||||
flavour: string,
|
||||
onWidgetConnected?: (component: WidgetComponent) => void
|
||||
) {
|
||||
class TempServiceWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = flavour;
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
const disposableGroup = this.blockService.disposables;
|
||||
if (onWidgetConnected) {
|
||||
disposableGroup.add(
|
||||
this.blockService.specSlots.widgetConnected.on(({ component }) => {
|
||||
onWidgetConnected(component);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TempServiceWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the block specs with custom renderers.
|
||||
*/
|
||||
export function patchReferenceRenderer(
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||
reactRenderer: ReferenceReactRenderer
|
||||
): ExtensionType {
|
||||
const customContent = (reference: AffineReference) => {
|
||||
const node = reactRenderer(reference);
|
||||
return reactToLit(node);
|
||||
};
|
||||
|
||||
return ReferenceNodeConfigExtension({
|
||||
customContent,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchNotificationService({
|
||||
closeConfirmModal,
|
||||
openConfirmModal,
|
||||
}: ReturnType<typeof useConfirmModal>) {
|
||||
return NotificationExtension({
|
||||
confirm: async ({ title, message, confirmText, cancelText, abort }) => {
|
||||
return new Promise<boolean>(resolve => {
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: toReactNode(message),
|
||||
confirmText,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(false);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
prompt: async ({
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
placeholder,
|
||||
cancelText,
|
||||
autofill,
|
||||
abort,
|
||||
}) => {
|
||||
return new Promise<string | null>(resolve => {
|
||||
let value = autofill || '';
|
||||
const description = (
|
||||
<div>
|
||||
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
|
||||
<Input
|
||||
autoSelect={true}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={e => (value = e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: description,
|
||||
confirmText: confirmText ?? 'Confirm',
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText: cancelText ?? 'Cancel',
|
||||
onConfirm: () => {
|
||||
resolve(value);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(null);
|
||||
},
|
||||
autoFocusConfirm: false,
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(null);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
toast: (message: string, options: ToastOptions) => {
|
||||
return toast(message, options);
|
||||
},
|
||||
notify: notification => {
|
||||
const accentToNotify = {
|
||||
error: notify.error,
|
||||
success: notify.success,
|
||||
warning: notify.warning,
|
||||
info: notify,
|
||||
};
|
||||
|
||||
const fn = accentToNotify[notification.accent || 'info'];
|
||||
if (!fn) {
|
||||
throw new Error('Invalid notification accent');
|
||||
}
|
||||
|
||||
const toastId = fn(
|
||||
{
|
||||
title: toReactNode(notification.title),
|
||||
message: toReactNode(notification.message),
|
||||
footer: toReactNode(notification.footer),
|
||||
action: notification.action?.onClick
|
||||
? {
|
||||
label: toReactNode(notification.action?.label),
|
||||
onClick: notification.action.onClick,
|
||||
}
|
||||
: undefined,
|
||||
onDismiss: notification.onClose,
|
||||
},
|
||||
{
|
||||
duration: notification.duration || 0,
|
||||
onDismiss: notification.onClose,
|
||||
onAutoClose: notification.onClose,
|
||||
}
|
||||
);
|
||||
|
||||
notification.abort?.addEventListener('abort', () => {
|
||||
notify.dismiss(toastId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchOpenDocExtension() {
|
||||
const openDocConfig: OpenDocConfig = {
|
||||
items: [
|
||||
{
|
||||
type: 'open-in-active-view',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc'](),
|
||||
icon: ExpandFullIcon(),
|
||||
},
|
||||
BUILD_CONFIG.isElectron
|
||||
? {
|
||||
type: 'open-in-new-view',
|
||||
label:
|
||||
I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||
icon: SplitViewIcon(),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
type: 'open-in-new-tab',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||
icon: OpenInNewIcon(),
|
||||
},
|
||||
{
|
||||
type: 'open-in-center-peek',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
||||
icon: CenterPeekIcon(),
|
||||
},
|
||||
].filter((item): item is OpenDocConfigItem => item !== null),
|
||||
};
|
||||
return OpenDocExtension(openDocConfig);
|
||||
}
|
||||
|
||||
export function patchPeekViewService(service: PeekViewService) {
|
||||
return PeekViewExtension({
|
||||
peek: (
|
||||
element: {
|
||||
target: HTMLElement;
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
template?: TemplateResult;
|
||||
},
|
||||
options?: PeekOptions
|
||||
) => {
|
||||
logger.debug('center peek', element);
|
||||
const { template, target, ...props } = element;
|
||||
|
||||
return service.peekView.open(
|
||||
{
|
||||
element: target,
|
||||
docRef: props,
|
||||
},
|
||||
template,
|
||||
options?.abortSignal
|
||||
);
|
||||
},
|
||||
} satisfies BSPeekViewService);
|
||||
}
|
||||
|
||||
export function patchDocModeService(
|
||||
docService: DocService,
|
||||
docsService: DocsService,
|
||||
editorService: EditorService
|
||||
): ExtensionType {
|
||||
const DEFAULT_MODE = 'page';
|
||||
class AffineDocModeService implements DocModeProvider {
|
||||
setEditorMode = (mode: DocMode) => {
|
||||
editorService.editor.setMode(mode);
|
||||
};
|
||||
getEditorMode = () => {
|
||||
return editorService.editor.mode$.value;
|
||||
};
|
||||
setPrimaryMode = (mode: DocMode, id?: string) => {
|
||||
if (id) {
|
||||
docsService.list.setPrimaryMode(id, mode);
|
||||
} else {
|
||||
docService.doc.setPrimaryMode(mode);
|
||||
}
|
||||
};
|
||||
getPrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.getPrimaryMode(id)
|
||||
: docService.doc.getPrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
togglePrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.togglePrimaryMode(id)
|
||||
: docService.doc.togglePrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
||||
const mode$ = id
|
||||
? docsService.list.primaryMode$(id)
|
||||
: docService.doc.primaryMode$;
|
||||
const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode));
|
||||
return {
|
||||
dispose: sub.unsubscribe,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const docModeExtension = DocModeExtension(new AffineDocModeService());
|
||||
|
||||
return docModeExtension;
|
||||
}
|
||||
|
||||
export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||
const QuickSearch = QuickSearchExtension({
|
||||
async openQuickSearch() {
|
||||
let searchResult: QuickSearchResult = null;
|
||||
searchResult = await new Promise((resolve, reject) =>
|
||||
framework.get(QuickSearchService).quickSearch.show(
|
||||
[
|
||||
framework.get(RecentDocsQuickSearchSession),
|
||||
framework.get(CreationQuickSearchSession),
|
||||
framework.get(DocsQuickSearchSession),
|
||||
framework.get(LinksQuickSearchSession),
|
||||
framework.get(ExternalLinksQuickSearchSession),
|
||||
framework.get(JournalsQuickSearchSession),
|
||||
],
|
||||
result => {
|
||||
if (result === null) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'docs') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'recent-doc') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'link') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
params: pick(result.payload, [
|
||||
'mode',
|
||||
'blockIds',
|
||||
'elementIds',
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'date-picker') {
|
||||
result.payload
|
||||
.getDocId()
|
||||
.then(docId => {
|
||||
if (docId) {
|
||||
resolve({ docId });
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'external-link') {
|
||||
const externalUrl = result.payload.url;
|
||||
resolve({ externalUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'creation') {
|
||||
const docsService = framework.get(DocsService);
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const mode =
|
||||
result.id === 'creation:create-edgeless' ? 'edgeless' : 'page';
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(result.payload.title) },
|
||||
note: editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
const newDoc = docsService.createDoc({
|
||||
primaryMode: mode,
|
||||
docProps,
|
||||
});
|
||||
|
||||
resolve({ docId: newDoc.id });
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: {
|
||||
i18nKey: 'com.affine.cmdk.insert-links',
|
||||
},
|
||||
placeholder: {
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return searchResult;
|
||||
},
|
||||
});
|
||||
|
||||
const SlashMenuQuickSearchExtension = patchSpecService(
|
||||
'affine:page',
|
||||
(component: WidgetComponent) => {
|
||||
if (component instanceof AffineSlashMenuWidget) {
|
||||
component.config.items.forEach(item => {
|
||||
if (
|
||||
'action' in item &&
|
||||
(item.name === 'Linked Doc' || item.name === 'Link')
|
||||
) {
|
||||
item.action = async ({ rootComponent }) => {
|
||||
const [success, { insertedLinkType }] =
|
||||
rootComponent.std.command.exec(insertLinkByQuickSearchCommand);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
if (flavour === 'affine:bookmark') {
|
||||
track.doc.editor.slashMenu.bookmark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (flavour === 'affine:embed-linked-doc') {
|
||||
track.doc.editor.slashMenu.linkDoc({
|
||||
control: 'linkDoc',
|
||||
});
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return [QuickSearch, SlashMenuQuickSearchExtension];
|
||||
}
|
||||
|
||||
export function patchParseDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const ParseDocUrl = ParseDocUrlExtension({
|
||||
parseDocUrl(url) {
|
||||
const info = resolveLinkToDoc(url);
|
||||
if (!info || info.workspaceId !== workspaceService.workspace.id) return;
|
||||
|
||||
delete info.refreshKey;
|
||||
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
return [ParseDocUrl];
|
||||
}
|
||||
|
||||
export function patchGenerateDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const GenerateDocUrl = GenerateDocUrlExtension({
|
||||
generateDocUrl(pageId: string, params?: ReferenceParams) {
|
||||
return generateUrl({
|
||||
...params,
|
||||
pageId,
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return [GenerateDocUrl];
|
||||
}
|
||||
|
||||
export function patchEdgelessClipboard() {
|
||||
class EdgelessClipboardWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this.blockService.disposables.add(
|
||||
this.blockService.specSlots.viewConnected.on(view => {
|
||||
const { component } = view;
|
||||
if (component instanceof EdgelessRootBlockComponent) {
|
||||
const AIChatBlockFlavour = AIChatBlockSchema.model.flavour;
|
||||
const createFunc = (block: BlockSnapshot) => {
|
||||
const {
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
} = block.props;
|
||||
const blockId = component.service.crud.addBlock(
|
||||
AIChatBlockFlavour,
|
||||
{
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
},
|
||||
component.surface.model.id
|
||||
);
|
||||
return blockId;
|
||||
};
|
||||
component.clipboardController.registerBlock(
|
||||
AIChatBlockFlavour,
|
||||
createFunc
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return EdgelessClipboardWatcher;
|
||||
}
|
||||
|
||||
@customElement('affine-linked-doc-ref-block')
|
||||
export class LinkedDocBlockComponent extends EmbedLinkedDocBlockComponent {
|
||||
override getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
isBannerEmpty: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function patchForSharedPage() {
|
||||
const extension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-linked-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-synced-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
},
|
||||
};
|
||||
return extension;
|
||||
}
|
||||
|
||||
export function patchForMobile() {
|
||||
class MobileSpecsPatches extends LifeCycleWatcher {
|
||||
static override key = 'mobile-patches';
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
|
||||
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
|
||||
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
|
||||
// Hide reference popup on mobile.
|
||||
{
|
||||
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
|
||||
di.override(ReferenceNodeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
hidePopup: true,
|
||||
} satisfies ReferenceNodeConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Hide number lines for code block on mobile.
|
||||
{
|
||||
const codeConfigIdentifier = ConfigIdentifier('affine:code');
|
||||
const prev = di.getFactory(codeConfigIdentifier);
|
||||
di.override(codeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
showLineNumbers: false,
|
||||
} satisfies CodeBlockConfig;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
// remove slash placeholder for mobile: `type / ...`
|
||||
{
|
||||
const paragraphService = this.std.get(ParagraphBlockService);
|
||||
if (!paragraphService) return;
|
||||
|
||||
paragraphService.placeholderGenerator = model => {
|
||||
const placeholders = {
|
||||
text: '',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.type];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const extensions: ExtensionType[] = [
|
||||
{
|
||||
setup: di => {
|
||||
const prev = di.getFactory(RootBlockConfigExtension.identifier);
|
||||
|
||||
di.override(RootBlockConfigExtension.identifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
keyboardToolbar: createKeyboardToolbarConfig(),
|
||||
} satisfies RootBlockConfig;
|
||||
});
|
||||
},
|
||||
},
|
||||
MobileSpecsPatches,
|
||||
];
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export function patchForAttachmentEmbedViews(
|
||||
reactToLit: (
|
||||
element: ElementOrFactory,
|
||||
rerendering?: boolean
|
||||
) => TemplateResult
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
|
||||
name: 'pdf',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type === 'application/pdf' && model.size <= maxFileSize,
|
||||
action: model => {
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
bound.w = 537 + 24 + 2;
|
||||
bound.h = 759 + 46 + 24 + 2;
|
||||
model.doc.updateBlock(model, {
|
||||
embed: true,
|
||||
style: 'pdf',
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
},
|
||||
template: (model, _blobUrl) =>
|
||||
reactToLit(<AttachmentEmbedPreview model={model} />, false),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchForClipboardInElectron(framework: FrameworkProvider) {
|
||||
const desktopApi = framework.get(DesktopApiService);
|
||||
return NativeClipboardExtension({
|
||||
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.doc.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(<BlocksuiteEditorJournalDocTitle page={note.doc} />);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchSideBarService(framework: FrameworkProvider) {
|
||||
const { workbench } = framework.get(WorkbenchService);
|
||||
|
||||
return SidebarExtension({
|
||||
open: (tabId?: string) => {
|
||||
workbench.openSidebar();
|
||||
workbench.activeView$.value.activeSidebarTab(tabId ?? null);
|
||||
},
|
||||
close: () => {
|
||||
workbench.closeSidebar();
|
||||
},
|
||||
getTabIds: () => {
|
||||
return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { enableAIExtension } from '@affine/core/blocksuite/ai';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless';
|
||||
import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers';
|
||||
import type { SpecBuilder, TemplateManager } from '@blocksuite/affine/blocks';
|
||||
import { EdgelessTemplatePanel, SpecProvider } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension } from './custom/root-block';
|
||||
|
||||
export function createEdgelessModeSpecs(
|
||||
framework: FrameworkProvider
|
||||
): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const edgelessSpec = SpecProvider._.getSpec('edgeless');
|
||||
enableAffineExtension(framework, edgelessSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(edgelessSpec, framework);
|
||||
}
|
||||
|
||||
return edgelessSpec;
|
||||
}
|
||||
|
||||
export function effects() {
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInStickersTemplates as TemplateManager
|
||||
);
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInEdgelessTemplates as TemplateManager
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { enableAIExtension } from '@affine/core/blocksuite/ai';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { type SpecBuilder, SpecProvider } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension } from './custom/root-block';
|
||||
|
||||
export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const provider = SpecProvider._;
|
||||
const pageSpec = provider.getSpec('page');
|
||||
enableAffineExtension(framework, pageSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(pageSpec, framework);
|
||||
}
|
||||
return pageSpec;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user