@blocksuite/editor
Advanced tools
Comparing version 0.3.1 to 0.4.0-20230110000526-9d95ace
@@ -1,4 +0,4 @@ | ||
import { Page } from '@blocksuite/store'; | ||
import { BaseBlockModel, Page } from '@blocksuite/store'; | ||
import type { MouseMode, PageBlockModel } from '@blocksuite/blocks'; | ||
import { NonShadowLitElement } from '@blocksuite/blocks'; | ||
import { NonShadowLitElement, SurfaceBlockModel } from '@blocksuite/blocks'; | ||
import { ClipboardManager, ContentParser } from '../managers/index.js'; | ||
@@ -10,5 +10,8 @@ export declare class EditorContainer extends NonShadowLitElement { | ||
mouseMode: MouseMode; | ||
showGrid: boolean; | ||
clipboard: ClipboardManager; | ||
contentParser: ContentParser; | ||
get model(): PageBlockModel; | ||
get model(): [PageBlockModel | null, BaseBlockModel<unknown> | null]; | ||
get pageBlockModel(): PageBlockModel | null; | ||
get surfaceBlockModel(): SurfaceBlockModel | null; | ||
private _placeholderInput; | ||
@@ -15,0 +18,0 @@ private _disposables; |
@@ -22,2 +22,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
}; | ||
this.showGrid = false; | ||
// TODO only select block | ||
@@ -29,4 +30,12 @@ this.clipboard = new ClipboardManager(this, this); | ||
get model() { | ||
return this.page.root; | ||
return [this.page.root, this.page.rootLayer]; | ||
} | ||
get pageBlockModel() { | ||
return Array.isArray(this.model) ? this.model[0] : this.model; | ||
} | ||
get surfaceBlockModel() { | ||
return Array.isArray(this.model) | ||
? this.model[1] | ||
: null; | ||
} | ||
// disable shadow DOM to workaround quill | ||
@@ -51,2 +60,5 @@ createRenderRoot() { | ||
})); | ||
this._disposables.add(Signal.fromEvent(window, 'affine:switch-edgeless-display-mode').on(({ detail }) => { | ||
this.showGrid = detail; | ||
})); | ||
// subscribe store | ||
@@ -70,3 +82,3 @@ this._disposables.add(this.page.signals.rootAdded.on(() => { | ||
.page=${this.page} | ||
.model=${this.model} | ||
.model=${this.pageBlockModel} | ||
.readonly=${this.readonly} | ||
@@ -79,5 +91,7 @@ ></affine-default-page> | ||
.page=${this.page} | ||
.model=${this.model} | ||
.pageModel=${this.pageBlockModel} | ||
.surfaceModel=${this.surfaceBlockModel} | ||
.mouseMode=${this.mouseMode} | ||
.readonly=${this.readonly} | ||
.showGrid=${this.showGrid} | ||
></affine-edgeless-page> | ||
@@ -118,2 +132,5 @@ `; | ||
state() | ||
], EditorContainer.prototype, "showGrid", void 0); | ||
__decorate([ | ||
state() | ||
], EditorContainer.prototype, "clipboard", void 0); | ||
@@ -120,0 +137,0 @@ __decorate([ |
@@ -7,5 +7,7 @@ export * from './components/index.js'; | ||
? window | ||
: typeof global !== 'undefined' | ||
? global | ||
: {}; | ||
: // @ts-ignore | ||
typeof global !== 'undefined' | ||
? // @ts-ignore | ||
global | ||
: {}; | ||
const importIdentifier = '__ $BLOCKSUITE_EDITOR$ __'; | ||
@@ -12,0 +14,0 @@ // @ts-ignore |
import { Signal } from '@blocksuite/store'; | ||
import type { OpenBlockInfo, EditorContainer, SelectedBlock } from '../../../index.js'; | ||
type ParseHtml2BlockFunc = (...args: any[]) => OpenBlockInfo[] | null; | ||
import type { EditorContainer, OpenBlockInfo, SelectedBlock } from '../../../index.js'; | ||
type ParseHtml2BlockFunc = (...args: any[]) => Promise<OpenBlockInfo[] | null>; | ||
export declare class ContentParser { | ||
@@ -10,3 +10,3 @@ private _editor; | ||
private _parsers; | ||
private _parseHtml; | ||
private _htmlParser; | ||
constructor(editor: EditorContainer); | ||
@@ -17,4 +17,4 @@ onExportHtml(): void; | ||
block2Text(blocks: SelectedBlock[]): string; | ||
htmlText2Block(html: string): OpenBlockInfo[]; | ||
markdown2Block(text: string): OpenBlockInfo[]; | ||
htmlText2Block(html: string): Promise<OpenBlockInfo[]>; | ||
markdown2Block(text: string): Promise<OpenBlockInfo[]>; | ||
registerParserHtmlText2Block(name: string, func: ParseHtml2BlockFunc): void; | ||
@@ -21,0 +21,0 @@ getParserHtmlText2Block(name: string): ParseHtml2BlockFunc; |
import { marked } from 'marked'; | ||
import { Signal } from '@blocksuite/store'; | ||
import { FileExporter } from '../../file-exporter/file-exporter.js'; | ||
import { ParserHtml } from './parse-html.js'; | ||
import { HtmlParser } from './parse-html.js'; | ||
export class ContentParser { | ||
@@ -12,4 +12,4 @@ constructor(editor) { | ||
this._editor = editor; | ||
this._parseHtml = new ParserHtml(this); | ||
this._parseHtml.registerParsers(); | ||
this._htmlParser = new HtmlParser(this, this._editor); | ||
this._htmlParser.registerParsers(); | ||
} | ||
@@ -20,3 +20,3 @@ onExportHtml() { | ||
return; | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children); | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children[0].children); | ||
FileExporter.exportHtml(root.title, htmlContent); | ||
@@ -28,3 +28,3 @@ } | ||
return; | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children); | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children[0].children); | ||
FileExporter.exportHtmlAsMarkdown(root.title, htmlContent); | ||
@@ -45,3 +45,3 @@ } | ||
} | ||
htmlText2Block(html) { | ||
async htmlText2Block(html) { | ||
const htmlEl = document.createElement('html'); | ||
@@ -53,3 +53,3 @@ htmlEl.innerHTML = html; | ||
} | ||
markdown2Block(text) { | ||
async markdown2Block(text) { | ||
const underline = { | ||
@@ -78,3 +78,26 @@ name: 'underline', | ||
}; | ||
marked.use({ extensions: [underline] }); | ||
const inlineCode = { | ||
name: 'inlineCode', | ||
level: 'inline', | ||
start(src) { | ||
return src.indexOf('`'); | ||
}, | ||
tokenizer(src) { | ||
const rule = /^(?:`)(`{2,}?|[^`]+)(?:`)$/g; | ||
const match = rule.exec(src); | ||
if (match) { | ||
return { | ||
type: 'inlineCode', | ||
raw: match[0], | ||
text: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer | ||
}; | ||
} | ||
return; | ||
}, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
renderer(token) { | ||
return `<code>${token.text}</code>`; | ||
}, | ||
}; | ||
marked.use({ extensions: [underline, inlineCode] }); | ||
const md2html = marked.parse(text); | ||
@@ -117,3 +140,17 @@ return this.htmlText2Block(md2html); | ||
const text = model.block2html(children.join(''), previousSibling?.id || '', nextSibling?.id || '', block.startPos, block.endPos); | ||
return text; | ||
switch (model.type) { | ||
case 'text': | ||
return `<p>${text}</p>`; | ||
case 'h1': | ||
case 'h2': | ||
case 'h3': | ||
case 'h4': | ||
case 'h5': | ||
case 'h6': | ||
return `<${model.type}>${text}</${model.type}>`; | ||
case 'quote': | ||
return `<blockquote>${text}</blockquote>`; | ||
default: | ||
return text; | ||
} | ||
} | ||
@@ -133,15 +170,18 @@ _getTextInfoBySelectionInfo(selectedBlock) { | ||
} | ||
_convertHtml2Blocks(element) { | ||
return Array.from(element.children) | ||
.map(childElement => { | ||
const clipBlockInfos = this.getParserHtmlText2Block('nodeParser')?.(childElement) || []; | ||
if (clipBlockInfos && clipBlockInfos.length) { | ||
async _convertHtml2Blocks(element) { | ||
const openBlockPromises = Array.from(element.children).map(async (childElement) => { | ||
const clipBlockInfos = (await this.getParserHtmlText2Block('nodeParser')?.(childElement)) || | ||
[]; | ||
if (clipBlockInfos.length) { | ||
return clipBlockInfos; | ||
} | ||
return []; | ||
}) | ||
.flat() | ||
.filter(v => v); | ||
}); | ||
const results = []; | ||
for (const item of openBlockPromises) { | ||
results.push(await item); | ||
} | ||
return results.flat().filter(v => v); | ||
} | ||
} | ||
//# sourceMappingURL=index.js.map |
import type { ContentParser } from './index.js'; | ||
export declare class ParserHtml { | ||
import type { EditorContainer } from '../../../components/index.js'; | ||
export declare class HtmlParser { | ||
private _contentParser; | ||
constructor(contentParser: ContentParser); | ||
private _editor; | ||
constructor(contentParser: ContentParser, editor: EditorContainer); | ||
registerParsers(): void; | ||
private _nodePaser; | ||
private _nodeParser; | ||
private _commonParser; | ||
@@ -12,3 +14,5 @@ private _commonHTML2Block; | ||
private _blockQuoteParser; | ||
private _codeBlockParser; | ||
private _embedItemParser; | ||
} | ||
//# sourceMappingURL=parse-html.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import { assertExists } from '@blocksuite/blocks'; | ||
// There are these uncommon in-line tags that have not been added | ||
@@ -22,103 +23,241 @@ // tt, acronym, dfn, kbd, samp, var, bdo, br, img, map, object, q, script, sub, sup, button, select, TEXTAREA | ||
]; | ||
export class ParserHtml { | ||
constructor(contentParser) { | ||
this._contentParser = contentParser; | ||
} | ||
registerParsers() { | ||
this._contentParser.registerParserHtmlText2Block('nodeParser', this._nodePaser.bind(this)); | ||
this._contentParser.registerParserHtmlText2Block('commonParser', this._commonParser.bind(this)); | ||
this._contentParser.registerParserHtmlText2Block('listItemParser', this._listItemParser.bind(this)); | ||
this._contentParser.registerParserHtmlText2Block('blockQuoteParser', this._blockQuoteParser.bind(this)); | ||
} | ||
// TODO parse children block | ||
_nodePaser(node) { | ||
let result; | ||
// custom parser | ||
result = | ||
this._contentParser.getParserHtmlText2Block('customNodeParser')?.(node); | ||
if (result && result.length > 0) { | ||
return result; | ||
} | ||
const tagName = node.tagName; | ||
if (node instanceof Text || INLINE_TAGS.includes(tagName)) { | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
} | ||
else { | ||
switch (tagName) { | ||
case 'H1': | ||
case 'H2': | ||
case 'H3': | ||
case 'H4': | ||
case 'H5': | ||
case 'H6': | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: tagName.toLowerCase(), | ||
}); | ||
break; | ||
case 'BLOCKQUOTE': | ||
result = | ||
this._contentParser.getParserHtmlText2Block('blockQuoteParser')?.(node); | ||
break; | ||
case 'P': | ||
if (node.firstChild instanceof Text && | ||
(node.firstChild.textContent?.startsWith('[] ') || | ||
node.firstChild.textContent?.startsWith('[ ] ') || | ||
node.firstChild.textContent?.startsWith('[x] '))) { | ||
result = | ||
this._contentParser.getParserHtmlText2Block('listItemParser')?.(node); | ||
} | ||
else { | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
export class HtmlParser { | ||
constructor(contentParser, editor) { | ||
// TODO parse children block | ||
this._nodeParser = async (node) => { | ||
let result; | ||
// custom parser | ||
result = await this._contentParser.getParserHtmlText2Block('customNodeParser')?.(node); | ||
if (result && result.length > 0) { | ||
return result; | ||
} | ||
const tagName = node.tagName; | ||
if (node instanceof Text || INLINE_TAGS.includes(tagName)) { | ||
result = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
} | ||
else { | ||
switch (tagName) { | ||
case 'H1': | ||
case 'H2': | ||
case 'H3': | ||
case 'H4': | ||
case 'H5': | ||
case 'H6': | ||
result = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: tagName.toLowerCase(), | ||
}); | ||
break; | ||
case 'BLOCKQUOTE': | ||
result = await this._contentParser.getParserHtmlText2Block('blockQuoteParser')?.(node); | ||
break; | ||
case 'P': | ||
if (node.firstChild instanceof Text && | ||
(node.firstChild.textContent?.startsWith('[] ') || | ||
node.firstChild.textContent?.startsWith('[ ] ') || | ||
node.firstChild.textContent?.startsWith('[x] '))) { | ||
result = await this._contentParser.getParserHtmlText2Block('listItemParser')?.(node); | ||
} | ||
else if (node.firstChild instanceof HTMLImageElement) { | ||
result = await this._contentParser.getParserHtmlText2Block('embedItemParser')?.(node.firstChild); | ||
} | ||
else { | ||
result = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
} | ||
break; | ||
case 'DIV': | ||
result = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
break; | ||
case 'LI': | ||
result = await this._contentParser.getParserHtmlText2Block('listItemParser')?.(node); | ||
break; | ||
case 'HR': | ||
result = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:divider', | ||
}); | ||
break; | ||
case 'PRE': | ||
result = await this._contentParser.getParserHtmlText2Block('codeBlockParser')?.(node); | ||
break; | ||
case 'IMG': | ||
{ | ||
result = await this._contentParser.getParserHtmlText2Block('embedItemParser')?.(node); | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
if (result && result.length > 0) { | ||
return result; | ||
} | ||
const openBlockPromises = Array.from(node.children).map(async (childElement) => { | ||
const clipBlockInfos = (await this._contentParser.getParserHtmlText2Block('nodeParser')?.(childElement)) || []; | ||
if (clipBlockInfos && clipBlockInfos.length) { | ||
return clipBlockInfos; | ||
} | ||
return []; | ||
}); | ||
const results = []; | ||
for (const item of openBlockPromises) { | ||
results.push(await item); | ||
} | ||
return results.flat().filter(v => v); | ||
}; | ||
this._commonParser = async ({ element, flavour, type, checked, ignoreEmptyElement = true, }) => { | ||
const res = await this._commonHTML2Block(element, flavour, type, checked, ignoreEmptyElement); | ||
return res ? [res] : null; | ||
}; | ||
this._listItemParser = async (element) => { | ||
const tagName = element.parentElement?.tagName; | ||
let type = tagName === 'OL' ? 'numbered' : 'bulleted'; | ||
let checked; | ||
let inputEl; | ||
if ((inputEl = element.firstElementChild)?.tagName === 'INPUT' || | ||
(inputEl = element.firstElementChild?.firstElementChild)?.tagName === | ||
'INPUT') { | ||
type = 'todo'; | ||
checked = inputEl?.getAttribute('checked') !== null; | ||
} | ||
if (element.firstChild instanceof Text) { | ||
if (element.firstChild.textContent?.startsWith('[] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(3); | ||
type = 'todo'; | ||
checked = false; | ||
} | ||
else if (element.firstChild.textContent?.startsWith('[ ] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(4); | ||
type = 'todo'; | ||
checked = false; | ||
} | ||
else if (element.firstChild.textContent?.startsWith('[x] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(4); | ||
type = 'todo'; | ||
checked = true; | ||
} | ||
} | ||
const result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: element, | ||
flavour: 'affine:list', | ||
type: type, | ||
checked: checked, | ||
}); | ||
return result; | ||
}; | ||
this._blockQuoteParser = async (element) => { | ||
const getText = (list) => { | ||
const result = []; | ||
list.forEach(item => { | ||
const texts = item.text.filter(textItem => textItem.insert); | ||
if (result.length > 0 && texts.length > 0) { | ||
result.push({ insert: '\n' }); | ||
} | ||
break; | ||
case 'DIV': | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
break; | ||
case 'LI': | ||
result = | ||
this._contentParser.getParserHtmlText2Block('listItemParser')?.(node); | ||
break; | ||
case 'HR': | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: node, | ||
flavour: 'affine:divider', | ||
}); | ||
break; | ||
default: | ||
break; | ||
result.push(...texts); | ||
const childTexts = getText(item.children); | ||
if (result.length > 0 && childTexts.length > 0) { | ||
result.push({ insert: '\n' }); | ||
} | ||
result.push(...childTexts); | ||
}); | ||
return result; | ||
}; | ||
const commonResult = await this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: element, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
if (!commonResult) { | ||
return null; | ||
} | ||
} | ||
if (result && result.length > 0) { | ||
return [ | ||
{ | ||
flavour: 'affine:paragraph', | ||
type: 'quote', | ||
text: getText(commonResult), | ||
children: [], | ||
}, | ||
]; | ||
}; | ||
this._codeBlockParser = async (element) => { | ||
// code block doesn't parse other nested Markdown syntax, thus is always one layer deep, example: | ||
// <pre><code class="language-typescript">code content</code></pre> | ||
const content = element.firstChild?.textContent || ''; | ||
const language = element.children[0]?.getAttribute('class')?.split('-')[1] || 'JavaScript'; | ||
return [ | ||
{ | ||
flavour: 'affine:code', | ||
type: 'code', | ||
text: [ | ||
{ | ||
insert: content, | ||
attributes: { | ||
'code-block': true, | ||
}, | ||
}, | ||
], | ||
children: [], | ||
language, | ||
}, | ||
]; | ||
}; | ||
this._embedItemParser = async (element) => { | ||
let result = []; | ||
if (element instanceof HTMLImageElement) { | ||
const imgUrl = element.src; | ||
let resp; | ||
try { | ||
resp = await fetch(imgUrl); | ||
} | ||
catch (error) { | ||
console.error(error); | ||
return result; | ||
} | ||
const imgBlob = await resp.blob(); | ||
if (!imgBlob.type.startsWith('image/')) { | ||
return result; | ||
} | ||
const storage = await this._editor.page.blobs; | ||
assertExists(storage); | ||
const id = await storage.set(imgBlob); | ||
result = [ | ||
{ | ||
flavour: 'affine:embed', | ||
type: 'image', | ||
sourceId: id, | ||
children: [], | ||
text: [{ insert: '' }], | ||
}, | ||
]; | ||
} | ||
return result; | ||
} | ||
return Array.from(node.children) | ||
.map(childElement => { | ||
const clipBlockInfos = this._contentParser.getParserHtmlText2Block('nodeParser')?.(childElement) || []; | ||
if (clipBlockInfos && clipBlockInfos.length) { | ||
return clipBlockInfos; | ||
} | ||
return []; | ||
}) | ||
.flat() | ||
.filter(v => v); | ||
}; | ||
this._contentParser = contentParser; | ||
this._editor = editor; | ||
} | ||
_commonParser({ element, flavour, type, checked, ignoreEmptyElement = true, }) { | ||
const res = this._commonHTML2Block(element, flavour, type, checked, ignoreEmptyElement); | ||
return res ? [res] : null; | ||
registerParsers() { | ||
this._contentParser.registerParserHtmlText2Block('nodeParser', this._nodeParser); | ||
this._contentParser.registerParserHtmlText2Block('commonParser', this._commonParser); | ||
this._contentParser.registerParserHtmlText2Block('listItemParser', this._listItemParser); | ||
this._contentParser.registerParserHtmlText2Block('blockQuoteParser', this._blockQuoteParser); | ||
this._contentParser.registerParserHtmlText2Block('codeBlockParser', this._codeBlockParser); | ||
this._contentParser.registerParserHtmlText2Block('embedItemParser', this._embedItemParser); | ||
} | ||
_commonHTML2Block(element, flavour, type, checked, ignoreEmptyElement = true) { | ||
async _commonHTML2Block(element, flavour, type, checked, ignoreEmptyElement = true) { | ||
const childNodes = element.childNodes; | ||
@@ -144,3 +283,3 @@ let isChildNode = false; | ||
if (node instanceof Element) { | ||
const childNode = this._nodePaser(node); | ||
const childNode = await this._nodeParser(node); | ||
childNode && children.push(...childNode); | ||
@@ -192,75 +331,2 @@ } | ||
} | ||
_listItemParser(element) { | ||
const tagName = element.parentElement?.tagName; | ||
let type = tagName === 'OL' ? 'numbered' : 'bulleted'; | ||
let checked; | ||
let inputEl; | ||
if ((inputEl = element.firstElementChild)?.tagName === 'INPUT' || | ||
(inputEl = element.firstElementChild?.firstElementChild)?.tagName === | ||
'INPUT') { | ||
type = 'todo'; | ||
checked = inputEl?.getAttribute('checked') !== null; | ||
} | ||
if (element.firstChild instanceof Text) { | ||
if (element.firstChild.textContent?.startsWith('[] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(3); | ||
type = 'todo'; | ||
checked = false; | ||
} | ||
else if (element.firstChild.textContent?.startsWith('[ ] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(4); | ||
type = 'todo'; | ||
checked = false; | ||
} | ||
else if (element.firstChild.textContent?.startsWith('[x] ')) { | ||
element.firstChild.textContent = | ||
element.firstChild.textContent.slice(4); | ||
type = 'todo'; | ||
checked = true; | ||
} | ||
} | ||
const result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: element, | ||
flavour: 'affine:list', | ||
type: type, | ||
checked: checked, | ||
}); | ||
return result; | ||
} | ||
_blockQuoteParser(element) { | ||
const getText = (list) => { | ||
const result = []; | ||
list.forEach(item => { | ||
const texts = item.text.filter(textItem => textItem.insert); | ||
if (result.length > 0 && texts.length > 0) { | ||
result.push({ insert: '\n' }); | ||
} | ||
result.push(...texts); | ||
const childTexts = getText(item.children); | ||
if (result.length > 0 && childTexts.length > 0) { | ||
result.push({ insert: '\n' }); | ||
} | ||
result.push(...childTexts); | ||
}); | ||
return result; | ||
}; | ||
const commonResult = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
element: element, | ||
flavour: 'affine:paragraph', | ||
type: 'text', | ||
}); | ||
if (!commonResult) { | ||
return null; | ||
} | ||
return [ | ||
{ | ||
flavour: 'affine:paragraph', | ||
type: 'quote', | ||
text: getText(commonResult), | ||
children: [], | ||
}, | ||
]; | ||
} | ||
} | ||
@@ -267,0 +333,0 @@ const getIsLink = (htmlElement) => { |
@@ -1,2 +0,2 @@ | ||
import { EmbedBlockModel, getCurrentRange, ListBlockModel, matchFlavours, SelectionUtils, } from '@blocksuite/blocks'; | ||
import { deleteModelsByRange, EmbedBlockModel, getCurrentRange, ListBlockModel, matchFlavours, SelectionUtils, } from '@blocksuite/blocks'; | ||
import { ClipboardItem } from './item.js'; | ||
@@ -27,23 +27,3 @@ import { CLIPBOARD_MIMETYPE } from './types.js'; | ||
this.handleCopy(e); | ||
// FIXME | ||
/* | ||
const { selectionInfo } = this._selection; | ||
if (selectionInfo.type == 'Block') { | ||
selectionInfo.blocks.forEach(({ id }) => | ||
this._editor.space.deleteBlockById(id) | ||
); | ||
} else if ( | ||
selectionInfo.type === 'Range' || | ||
selectionInfo.type === 'Caret' | ||
) { | ||
// TODO the selection of discontinuous and cross blocks are not exist yet | ||
this._editor.space.richTextAdapters | ||
.get(selectionInfo.anchorBlockId) | ||
?.quill.deleteText( | ||
selectionInfo.anchorBlockPosition || 0, | ||
(selectionInfo.focusBlockPosition || 0) - | ||
(selectionInfo.anchorBlockPosition || 0) | ||
); | ||
} | ||
*/ | ||
deleteModelsByRange(this._editor.page); | ||
} | ||
@@ -50,0 +30,0 @@ _getClipItems() { |
@@ -0,1 +1,2 @@ | ||
import { isPageTitle } from '@blocksuite/blocks/std'; | ||
import { Signal } from '@blocksuite/store'; | ||
@@ -43,5 +44,11 @@ import { ClipboardAction } from './types.js'; | ||
_copyHandler(e) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
this.signals.copy.emit(e); | ||
} | ||
_cutHandler(e) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
if (ClipboardEventDispatcher.editorElementActive()) { | ||
@@ -52,2 +59,5 @@ this.signals.cut.emit(e); | ||
_pasteHandler(e) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
if (ClipboardEventDispatcher.editorElementActive()) { | ||
@@ -54,0 +64,0 @@ this.signals.paste.emit(e); |
@@ -13,5 +13,5 @@ import type { EditorContainer } from '../../components/index.js'; | ||
get clipboardTarget(): HTMLElement; | ||
importMarkdown(text: string, insertPositionId: string): void; | ||
importMarkdown(text: string, insertPositionId: string): Promise<void>; | ||
dispose(): void; | ||
} | ||
//# sourceMappingURL=index.d.ts.map |
@@ -25,4 +25,4 @@ import { ClipboardEventDispatcher } from './event-dispatcher.js'; | ||
} | ||
importMarkdown(text, insertPositionId) { | ||
const blocks = this._editor.contentParser.markdown2Block(text); | ||
async importMarkdown(text, insertPositionId) { | ||
const blocks = await this._editor.contentParser.markdown2Block(text); | ||
this._paste.insertBlocks(blocks, { | ||
@@ -29,0 +29,0 @@ type: 'Block', |
@@ -83,3 +83,3 @@ import { MarkdownUtils } from './markdown-utils.js'; | ||
if (shouldConvertMarkdown) { | ||
return this._editor.contentParser.markdown2Block(textClipData); | ||
return await this._editor.contentParser.markdown2Block(textClipData); | ||
} | ||
@@ -142,11 +142,11 @@ return this._editor.contentParser.text2blocks(textClipData); | ||
if (matchFlavours(selectedBlock, ['affine:page'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:group'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:frame'])) { | ||
parent = selectedBlock.children[0]; | ||
} | ||
else { | ||
const id = this._editor.page.addBlock({ flavour: 'affine:group' }, selectedBlock.id); | ||
const id = this._editor.page.addBlock({ flavour: 'affine:frame' }, selectedBlock.id); | ||
parent = this._editor.page.getBlockById(id); | ||
} | ||
} | ||
else if (!matchFlavours(selectedBlock, ['affine:group'])) { | ||
else if (!matchFlavours(selectedBlock, ['affine:frame'])) { | ||
parent = this._editor.page.getParent(selectedBlock); | ||
@@ -158,3 +158,3 @@ index = (parent?.children.indexOf(selectedBlock) || 0) + 1; | ||
if (selectedBlock && !matchFlavours(selectedBlock, ['affine:page'])) { | ||
const endIndex = lastBlock.endPos || selectedBlock?.text?.length || 0; | ||
const endIndex = lastBlock.endPos ?? (selectedBlock?.text?.length || 0); | ||
const insertTexts = blocks[0].text; | ||
@@ -216,11 +216,11 @@ const insertLen = insertTexts.reduce((len, value) => { | ||
if (matchFlavours(selectedBlock, ['affine:page'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:group'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:frame'])) { | ||
parent = selectedBlock.children[0]; | ||
} | ||
else { | ||
const id = this._editor.page.addBlock({ flavour: 'affine:group' }, selectedBlock.id); | ||
const id = this._editor.page.addBlock({ flavour: 'affine:frame' }, selectedBlock.id); | ||
parent = this._editor.page.getBlockById(id); | ||
} | ||
} | ||
else if (!matchFlavours(selectedBlock, ['affine:group'])) { | ||
else if (!matchFlavours(selectedBlock, ['affine:frame'])) { | ||
parent = this._editor.page.getParent(selectedBlock); | ||
@@ -247,2 +247,3 @@ index = (parent?.children.indexOf(selectedBlock) || 0) + 1; | ||
height: block.height, | ||
language: block.language, | ||
}; | ||
@@ -249,0 +250,0 @@ const id = this._editor.page.addBlock(blockProps, parent, index + i); |
@@ -36,3 +36,4 @@ export declare enum CLIPBOARD_MIMETYPE { | ||
height?: number; | ||
language?: string; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable no-control-regex */ | ||
import TurndownService from 'turndown'; | ||
@@ -30,7 +31,4 @@ import { globalCSS, highlightCSS } from './exporter-style.js'; | ||
element.setAttribute('href', 'data:' + mimeType + ';charset=utf-8,' + encodeURIComponent(text)); | ||
// Consider if we should replace invalid characters in filenames before downloading, or if the browser | ||
// will do that for us automatically... | ||
// // replace illegal characters that cannot appear in file names | ||
// const safeFilename = filename.replace(/[ <>:/|?*]+/g, " ") | ||
element.setAttribute('download', filename); | ||
const safeFilename = getSafeFileName(filename); | ||
element.setAttribute('download', safeFilename); | ||
element.style.display = 'none'; | ||
@@ -95,2 +93,33 @@ document.body.appendChild(element); | ||
} | ||
function getSafeFileName(string) { | ||
const replacement = ' '; | ||
const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g; | ||
const windowsReservedNameRegex = /^(con|prn|aux|nul|com\d|lpt\d)$/i; | ||
const reControlChars = /[\u0000-\u001F\u0080-\u009F]/g; | ||
const reTrailingPeriods = /\.+$/; | ||
const allowedLength = 50; | ||
function trimRepeated(string, target) { | ||
const escapeStringRegexp = target | ||
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') | ||
.replace(/-/g, '\\x2d'); | ||
const regex = new RegExp(`(?:${escapeStringRegexp}){2,}`, 'g'); | ||
return string.replace(regex, target); | ||
} | ||
string = string | ||
.normalize('NFD') | ||
.replace(filenameReservedRegex, replacement) | ||
.replace(reControlChars, replacement) | ||
.replace(reTrailingPeriods, ''); | ||
string = trimRepeated(string, replacement); | ||
string = windowsReservedNameRegex.test(string) | ||
? string + replacement | ||
: string; | ||
const extIndex = string.lastIndexOf('.'); | ||
const filename = string.slice(0, extIndex).trim(); | ||
const extension = string.slice(extIndex); | ||
string = | ||
filename.slice(0, Math.max(1, allowedLength - extension.length)) + | ||
extension; | ||
return string; | ||
} | ||
//# sourceMappingURL=file-exporter.js.map |
{ | ||
"name": "@blocksuite/editor", | ||
"version": "0.3.1", | ||
"version": "0.4.0-20230110000526-9d95ace", | ||
"description": "Default BlockSuite-based editor built for AFFiNE.", | ||
@@ -11,4 +11,4 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@blocksuite/blocks": "0.3.1", | ||
"@blocksuite/store": "0.3.1", | ||
"@blocksuite/blocks": "0.4.0-20230110000526-9d95ace", | ||
"@blocksuite/store": "0.4.0-20230110000526-9d95ace", | ||
"lit": "^2.5.0", | ||
@@ -15,0 +15,0 @@ "marked": "^4.2.5", |
@@ -7,2 +7,2 @@ # `@blocksuite/editor` | ||
WIP | ||
TBD |
@@ -5,6 +5,6 @@ import { html } from 'lit'; | ||
import { Page, Signal } from '@blocksuite/store'; | ||
import { BaseBlockModel, Page, Signal } from '@blocksuite/store'; | ||
import { DisposableGroup } from '@blocksuite/store'; | ||
import type { MouseMode, PageBlockModel } from '@blocksuite/blocks'; | ||
import { NonShadowLitElement } from '@blocksuite/blocks'; | ||
import { NonShadowLitElement, SurfaceBlockModel } from '@blocksuite/blocks'; | ||
import { ClipboardManager, ContentParser } from '../managers/index.js'; | ||
@@ -28,2 +28,5 @@ | ||
@state() | ||
showGrid = false; | ||
// TODO only select block | ||
@@ -37,5 +40,18 @@ @state() | ||
get model() { | ||
return this.page.root as PageBlockModel; | ||
return [this.page.root, this.page.rootLayer] as [ | ||
PageBlockModel | null, | ||
BaseBlockModel | null | ||
]; | ||
} | ||
get pageBlockModel(): PageBlockModel | null { | ||
return Array.isArray(this.model) ? this.model[0] : this.model; | ||
} | ||
get surfaceBlockModel(): SurfaceBlockModel | null { | ||
return Array.isArray(this.model) | ||
? (this.model[1] as SurfaceBlockModel) | ||
: null; | ||
} | ||
@query('.affine-block-placeholder-input') | ||
@@ -74,2 +90,10 @@ private _placeholderInput!: HTMLInputElement; | ||
this._disposables.add( | ||
Signal.fromEvent(window, 'affine:switch-edgeless-display-mode').on( | ||
({ detail }) => { | ||
this.showGrid = detail; | ||
} | ||
) | ||
); | ||
// subscribe store | ||
@@ -98,3 +122,3 @@ this._disposables.add( | ||
.page=${this.page} | ||
.model=${this.model} | ||
.model=${this.pageBlockModel} | ||
.readonly=${this.readonly} | ||
@@ -108,5 +132,7 @@ ></affine-default-page> | ||
.page=${this.page} | ||
.model=${this.model} | ||
.pageModel=${this.pageBlockModel} | ||
.surfaceModel=${this.surfaceBlockModel} | ||
.mouseMode=${this.mouseMode} | ||
.readonly=${this.readonly} | ||
.showGrid=${this.showGrid} | ||
></affine-edgeless-page> | ||
@@ -113,0 +139,0 @@ `; |
@@ -8,4 +8,6 @@ export * from './components/index.js'; | ||
? window | ||
: typeof global !== 'undefined' | ||
? global | ||
: // @ts-ignore | ||
typeof global !== 'undefined' | ||
? // @ts-ignore | ||
global | ||
: {}; | ||
@@ -12,0 +14,0 @@ const importIdentifier = '__ $BLOCKSUITE_EDITOR$ __'; |
@@ -5,11 +5,11 @@ import { marked } from 'marked'; | ||
import type { | ||
EditorContainer, | ||
OpenBlockInfo, | ||
EditorContainer, | ||
SelectedBlock, | ||
} from '../../../index.js'; | ||
import { FileExporter } from '../../file-exporter/file-exporter.js'; | ||
import { ParserHtml } from './parse-html.js'; | ||
import { HtmlParser } from './parse-html.js'; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type ParseHtml2BlockFunc = (...args: any[]) => OpenBlockInfo[] | null; | ||
type ParseHtml2BlockFunc = (...args: any[]) => Promise<OpenBlockInfo[] | null>; | ||
@@ -22,7 +22,8 @@ export class ContentParser { | ||
private _parsers: Record<string, ParseHtml2BlockFunc> = {}; | ||
private _parseHtml: ParserHtml; | ||
private _htmlParser: HtmlParser; | ||
constructor(editor: EditorContainer) { | ||
this._editor = editor; | ||
this._parseHtml = new ParserHtml(this); | ||
this._parseHtml.registerParsers(); | ||
this._htmlParser = new HtmlParser(this, this._editor); | ||
this._htmlParser.registerParsers(); | ||
} | ||
@@ -33,3 +34,5 @@ | ||
if (!root) return; | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children); | ||
const htmlContent = this.block2Html( | ||
this._getSelectedBlock(root).children[0].children | ||
); | ||
FileExporter.exportHtml((root as PageBlockModel).title, htmlContent); | ||
@@ -41,3 +44,5 @@ } | ||
if (!root) return; | ||
const htmlContent = this.block2Html(this._getSelectedBlock(root).children); | ||
const htmlContent = this.block2Html( | ||
this._getSelectedBlock(root).children[0].children | ||
); | ||
FileExporter.exportHtmlAsMarkdown( | ||
@@ -73,3 +78,3 @@ (root as PageBlockModel).title, | ||
public htmlText2Block(html: string): OpenBlockInfo[] { | ||
public async htmlText2Block(html: string): Promise<OpenBlockInfo[]> { | ||
const htmlEl = document.createElement('html'); | ||
@@ -82,3 +87,3 @@ htmlEl.innerHTML = html; | ||
public markdown2Block(text: string): OpenBlockInfo[] { | ||
public async markdown2Block(text: string): Promise<OpenBlockInfo[]> { | ||
const underline = { | ||
@@ -107,3 +112,26 @@ name: 'underline', | ||
}; | ||
marked.use({ extensions: [underline] }); | ||
const inlineCode = { | ||
name: 'inlineCode', | ||
level: 'inline', | ||
start(src: string) { | ||
return src.indexOf('`'); | ||
}, | ||
tokenizer(src: string) { | ||
const rule = /^(?:`)(`{2,}?|[^`]+)(?:`)$/g; | ||
const match = rule.exec(src); | ||
if (match) { | ||
return { | ||
type: 'inlineCode', | ||
raw: match[0], // This is the text that you want your token to consume from the source | ||
text: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer | ||
}; | ||
} | ||
return; | ||
}, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
renderer(token: any) { | ||
return `<code>${token.text}</code>`; | ||
}, | ||
}; | ||
marked.use({ extensions: [underline, inlineCode] }); | ||
const md2html = marked.parse(text); | ||
@@ -171,3 +199,17 @@ return this.htmlText2Block(md2html); | ||
return text; | ||
switch (model.type) { | ||
case 'text': | ||
return `<p>${text}</p>`; | ||
case 'h1': | ||
case 'h2': | ||
case 'h3': | ||
case 'h4': | ||
case 'h5': | ||
case 'h6': | ||
return `<${model.type}>${text}</${model.type}>`; | ||
case 'quote': | ||
return `<blockquote>${text}</blockquote>`; | ||
default: | ||
return text; | ||
} | ||
} | ||
@@ -196,16 +238,24 @@ | ||
private _convertHtml2Blocks(element: Element): OpenBlockInfo[] { | ||
return Array.from(element.children) | ||
.map(childElement => { | ||
private async _convertHtml2Blocks( | ||
element: Element | ||
): Promise<OpenBlockInfo[]> { | ||
const openBlockPromises = Array.from(element.children).map( | ||
async childElement => { | ||
const clipBlockInfos = | ||
this.getParserHtmlText2Block('nodeParser')?.(childElement) || []; | ||
if (clipBlockInfos && clipBlockInfos.length) { | ||
(await this.getParserHtmlText2Block('nodeParser')?.(childElement)) || | ||
[]; | ||
if (clipBlockInfos.length) { | ||
return clipBlockInfos; | ||
} | ||
return []; | ||
}) | ||
.flat() | ||
.filter(v => v); | ||
} | ||
); | ||
const results: Array<OpenBlockInfo[]> = []; | ||
for (const item of openBlockPromises) { | ||
results.push(await item); | ||
} | ||
return results.flat().filter(v => v); | ||
} | ||
} |
import type { ContentParser } from './index.js'; | ||
import type { OpenBlockInfo } from '../types.js'; | ||
import type { EditorContainer } from '../../../components/index.js'; | ||
import { assertExists } from '@blocksuite/blocks'; | ||
@@ -26,6 +28,9 @@ // There are these uncommon in-line tags that have not been added | ||
export class ParserHtml { | ||
export class HtmlParser { | ||
private _contentParser: ContentParser; | ||
constructor(contentParser: ContentParser) { | ||
private _editor: EditorContainer; | ||
constructor(contentParser: ContentParser, editor: EditorContainer) { | ||
this._contentParser = contentParser; | ||
this._editor = editor; | ||
} | ||
@@ -36,23 +41,35 @@ | ||
'nodeParser', | ||
this._nodePaser.bind(this) | ||
this._nodeParser | ||
); | ||
this._contentParser.registerParserHtmlText2Block( | ||
'commonParser', | ||
this._commonParser.bind(this) | ||
this._commonParser | ||
); | ||
this._contentParser.registerParserHtmlText2Block( | ||
'listItemParser', | ||
this._listItemParser.bind(this) | ||
this._listItemParser | ||
); | ||
this._contentParser.registerParserHtmlText2Block( | ||
'blockQuoteParser', | ||
this._blockQuoteParser.bind(this) | ||
this._blockQuoteParser | ||
); | ||
this._contentParser.registerParserHtmlText2Block( | ||
'codeBlockParser', | ||
this._codeBlockParser | ||
); | ||
this._contentParser.registerParserHtmlText2Block( | ||
'embedItemParser', | ||
this._embedItemParser | ||
); | ||
} | ||
// TODO parse children block | ||
private _nodePaser(node: Element): OpenBlockInfo[] | null { | ||
private _nodeParser = async ( | ||
node: Element | ||
): Promise<OpenBlockInfo[] | null> => { | ||
let result; | ||
// custom parser | ||
result = | ||
this._contentParser.getParserHtmlText2Block('customNodeParser')?.(node); | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'customNodeParser' | ||
)?.(node); | ||
if (result && result.length > 0) { | ||
@@ -64,3 +81,5 @@ return result; | ||
if (node instanceof Text || INLINE_TAGS.includes(tagName)) { | ||
result = this._contentParser.getParserHtmlText2Block('commonParser')?.({ | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
)?.({ | ||
element: node, | ||
@@ -78,3 +97,3 @@ flavour: 'affine:paragraph', | ||
case 'H6': | ||
result = this._contentParser.getParserHtmlText2Block( | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
@@ -88,6 +107,5 @@ )?.({ | ||
case 'BLOCKQUOTE': | ||
result = | ||
this._contentParser.getParserHtmlText2Block('blockQuoteParser')?.( | ||
node | ||
); | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'blockQuoteParser' | ||
)?.(node); | ||
break; | ||
@@ -101,8 +119,11 @@ case 'P': | ||
) { | ||
result = | ||
this._contentParser.getParserHtmlText2Block('listItemParser')?.( | ||
node | ||
); | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'listItemParser' | ||
)?.(node); | ||
} else if (node.firstChild instanceof HTMLImageElement) { | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'embedItemParser' | ||
)?.(node.firstChild); | ||
} else { | ||
result = this._contentParser.getParserHtmlText2Block( | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
@@ -117,3 +138,3 @@ )?.({ | ||
case 'DIV': | ||
result = this._contentParser.getParserHtmlText2Block( | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
@@ -127,9 +148,8 @@ )?.({ | ||
case 'LI': | ||
result = | ||
this._contentParser.getParserHtmlText2Block('listItemParser')?.( | ||
node | ||
); | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'listItemParser' | ||
)?.(node); | ||
break; | ||
case 'HR': | ||
result = this._contentParser.getParserHtmlText2Block( | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
@@ -141,2 +161,14 @@ )?.({ | ||
break; | ||
case 'PRE': | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'codeBlockParser' | ||
)?.(node); | ||
break; | ||
case 'IMG': | ||
{ | ||
result = await this._contentParser.getParserHtmlText2Block( | ||
'embedItemParser' | ||
)?.(node); | ||
} | ||
break; | ||
default: | ||
@@ -150,8 +182,8 @@ break; | ||
} | ||
return Array.from(node.children) | ||
.map(childElement => { | ||
const openBlockPromises = Array.from(node.children).map( | ||
async childElement => { | ||
const clipBlockInfos = | ||
this._contentParser.getParserHtmlText2Block('nodeParser')?.( | ||
(await this._contentParser.getParserHtmlText2Block('nodeParser')?.( | ||
childElement | ||
) || []; | ||
)) || []; | ||
@@ -162,8 +194,13 @@ if (clipBlockInfos && clipBlockInfos.length) { | ||
return []; | ||
}) | ||
.flat() | ||
.filter(v => v); | ||
} | ||
} | ||
); | ||
private _commonParser({ | ||
const results: Array<OpenBlockInfo[]> = []; | ||
for (const item of openBlockPromises) { | ||
results.push(await item); | ||
} | ||
return results.flat().filter(v => v); | ||
}; | ||
private _commonParser = async ({ | ||
element, | ||
@@ -180,4 +217,4 @@ flavour, | ||
ignoreEmptyElement?: boolean; | ||
}): OpenBlockInfo[] | null { | ||
const res = this._commonHTML2Block( | ||
}): Promise<OpenBlockInfo[] | null> => { | ||
const res = await this._commonHTML2Block( | ||
element, | ||
@@ -190,5 +227,5 @@ flavour, | ||
return res ? [res] : null; | ||
} | ||
}; | ||
private _commonHTML2Block( | ||
private async _commonHTML2Block( | ||
element: Element, | ||
@@ -199,3 +236,3 @@ flavour: string, | ||
ignoreEmptyElement = true | ||
): OpenBlockInfo | null { | ||
): Promise<OpenBlockInfo | null> { | ||
const childNodes = element.childNodes; | ||
@@ -224,3 +261,3 @@ let isChildNode = false; | ||
if (node instanceof Element) { | ||
const childNode = this._nodePaser(node); | ||
const childNode = await this._nodeParser(node); | ||
childNode && children.push(...childNode); | ||
@@ -285,3 +322,5 @@ } | ||
private _listItemParser(element: Element): OpenBlockInfo[] | null { | ||
private _listItemParser = async ( | ||
element: Element | ||
): Promise<OpenBlockInfo[] | null> => { | ||
const tagName = element.parentElement?.tagName; | ||
@@ -326,5 +365,7 @@ let type = tagName === 'OL' ? 'numbered' : 'bulleted'; | ||
return result; | ||
} | ||
}; | ||
private _blockQuoteParser(element: Element): OpenBlockInfo[] | null { | ||
private _blockQuoteParser = async ( | ||
element: Element | ||
): Promise<OpenBlockInfo[] | null> => { | ||
const getText = (list: OpenBlockInfo[]): Record<string, unknown>[] => { | ||
@@ -350,3 +391,3 @@ const result: Record<string, unknown>[] = []; | ||
const commonResult = this._contentParser.getParserHtmlText2Block( | ||
const commonResult = await this._contentParser.getParserHtmlText2Block( | ||
'commonParser' | ||
@@ -370,3 +411,63 @@ )?.({ | ||
]; | ||
} | ||
}; | ||
private _codeBlockParser = async ( | ||
element: Element | ||
): Promise<OpenBlockInfo[] | null> => { | ||
// code block doesn't parse other nested Markdown syntax, thus is always one layer deep, example: | ||
// <pre><code class="language-typescript">code content</code></pre> | ||
const content = element.firstChild?.textContent || ''; | ||
const language = | ||
element.children[0]?.getAttribute('class')?.split('-')[1] || 'JavaScript'; | ||
return [ | ||
{ | ||
flavour: 'affine:code', | ||
type: 'code', | ||
text: [ | ||
{ | ||
insert: content, | ||
attributes: { | ||
'code-block': true, | ||
}, | ||
}, | ||
], | ||
children: [], | ||
language, | ||
}, | ||
]; | ||
}; | ||
private _embedItemParser = async ( | ||
element: Element | ||
): Promise<OpenBlockInfo[] | null> => { | ||
let result: OpenBlockInfo[] | null = []; | ||
if (element instanceof HTMLImageElement) { | ||
const imgUrl = (element as HTMLImageElement).src; | ||
let resp; | ||
try { | ||
resp = await fetch(imgUrl); | ||
} catch (error) { | ||
console.error(error); | ||
return result; | ||
} | ||
const imgBlob = await resp.blob(); | ||
if (!imgBlob.type.startsWith('image/')) { | ||
return result; | ||
} | ||
const storage = await this._editor.page.blobs; | ||
assertExists(storage); | ||
const id = await storage.set(imgBlob); | ||
result = [ | ||
{ | ||
flavour: 'affine:embed', | ||
type: 'image', | ||
sourceId: id, | ||
children: [], | ||
text: [{ insert: '' }], | ||
}, | ||
]; | ||
} | ||
return result; | ||
}; | ||
} | ||
@@ -373,0 +474,0 @@ |
import { | ||
deleteModelsByRange, | ||
EmbedBlockModel, | ||
@@ -41,23 +42,3 @@ getCurrentRange, | ||
this.handleCopy(e); | ||
// FIXME | ||
/* | ||
const { selectionInfo } = this._selection; | ||
if (selectionInfo.type == 'Block') { | ||
selectionInfo.blocks.forEach(({ id }) => | ||
this._editor.space.deleteBlockById(id) | ||
); | ||
} else if ( | ||
selectionInfo.type === 'Range' || | ||
selectionInfo.type === 'Caret' | ||
) { | ||
// TODO the selection of discontinuous and cross blocks are not exist yet | ||
this._editor.space.richTextAdapters | ||
.get(selectionInfo.anchorBlockId) | ||
?.quill.deleteText( | ||
selectionInfo.anchorBlockPosition || 0, | ||
(selectionInfo.focusBlockPosition || 0) - | ||
(selectionInfo.anchorBlockPosition || 0) | ||
); | ||
} | ||
*/ | ||
deleteModelsByRange(this._editor.page); | ||
} | ||
@@ -64,0 +45,0 @@ |
@@ -0,1 +1,2 @@ | ||
import { isPageTitle } from '@blocksuite/blocks/std'; | ||
import { Signal } from '@blocksuite/store'; | ||
@@ -58,2 +59,5 @@ import { ClipboardAction } from './types.js'; | ||
private _copyHandler(e: ClipboardEvent) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
this.signals.copy.emit(e); | ||
@@ -63,2 +67,5 @@ } | ||
private _cutHandler(e: ClipboardEvent) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
if (ClipboardEventDispatcher.editorElementActive()) { | ||
@@ -69,2 +76,5 @@ this.signals.cut.emit(e); | ||
private _pasteHandler(e: ClipboardEvent) { | ||
if (isPageTitle(e)) { | ||
return; | ||
} | ||
if (ClipboardEventDispatcher.editorElementActive()) { | ||
@@ -71,0 +81,0 @@ this.signals.paste.emit(e); |
@@ -43,4 +43,4 @@ import type { EditorContainer } from '../../components/index.js'; | ||
public importMarkdown(text: string, insertPositionId: string) { | ||
const blocks = this._editor.contentParser.markdown2Block(text); | ||
public async importMarkdown(text: string, insertPositionId: string) { | ||
const blocks = await this._editor.contentParser.markdown2Block(text); | ||
this._paste.insertBlocks(blocks, { | ||
@@ -47,0 +47,0 @@ type: 'Block', |
@@ -121,3 +121,3 @@ import type { BaseBlockModel } from '@blocksuite/store'; | ||
if (shouldConvertMarkdown) { | ||
return this._editor.contentParser.markdown2Block(textClipData); | ||
return await this._editor.contentParser.markdown2Block(textClipData); | ||
} | ||
@@ -197,7 +197,7 @@ | ||
if (matchFlavours(selectedBlock, ['affine:page'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:group'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:frame'])) { | ||
parent = selectedBlock.children[0]; | ||
} else { | ||
const id = this._editor.page.addBlock( | ||
{ flavour: 'affine:group' }, | ||
{ flavour: 'affine:frame' }, | ||
selectedBlock.id | ||
@@ -207,3 +207,3 @@ ); | ||
} | ||
} else if (!matchFlavours(selectedBlock, ['affine:group'])) { | ||
} else if (!matchFlavours(selectedBlock, ['affine:frame'])) { | ||
parent = this._editor.page.getParent(selectedBlock); | ||
@@ -215,3 +215,3 @@ index = (parent?.children.indexOf(selectedBlock) || 0) + 1; | ||
if (selectedBlock && !matchFlavours(selectedBlock, ['affine:page'])) { | ||
const endIndex = lastBlock.endPos || selectedBlock?.text?.length || 0; | ||
const endIndex = lastBlock.endPos ?? (selectedBlock?.text?.length || 0); | ||
const insertTexts = blocks[0].text; | ||
@@ -289,7 +289,7 @@ const insertLen = insertTexts.reduce( | ||
if (matchFlavours(selectedBlock, ['affine:page'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:group'])) { | ||
if (matchFlavours(selectedBlock.children[0], ['affine:frame'])) { | ||
parent = selectedBlock.children[0]; | ||
} else { | ||
const id = this._editor.page.addBlock( | ||
{ flavour: 'affine:group' }, | ||
{ flavour: 'affine:frame' }, | ||
selectedBlock.id | ||
@@ -299,3 +299,3 @@ ); | ||
} | ||
} else if (!matchFlavours(selectedBlock, ['affine:group'])) { | ||
} else if (!matchFlavours(selectedBlock, ['affine:frame'])) { | ||
parent = this._editor.page.getParent(selectedBlock); | ||
@@ -328,2 +328,3 @@ index = (parent?.children.indexOf(selectedBlock) || 0) + 1; | ||
height: block.height, | ||
language: block.language, | ||
}; | ||
@@ -330,0 +331,0 @@ const id = this._editor.page.addBlock(blockProps, parent, index + i); |
@@ -39,2 +39,3 @@ export enum CLIPBOARD_MIMETYPE { | ||
height?: number; | ||
language?: string; | ||
}; |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable no-control-regex */ | ||
import TurndownService from 'turndown'; | ||
@@ -35,7 +36,4 @@ import { globalCSS, highlightCSS } from './exporter-style.js'; | ||
); | ||
// Consider if we should replace invalid characters in filenames before downloading, or if the browser | ||
// will do that for us automatically... | ||
// // replace illegal characters that cannot appear in file names | ||
// const safeFilename = filename.replace(/[ <>:/|?*]+/g, " ") | ||
element.setAttribute('download', filename); | ||
const safeFilename = getSafeFileName(filename); | ||
element.setAttribute('download', safeFilename); | ||
@@ -110,1 +108,36 @@ element.style.display = 'none'; | ||
} | ||
function getSafeFileName(string: string) { | ||
const replacement = ' '; | ||
const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g; | ||
const windowsReservedNameRegex = /^(con|prn|aux|nul|com\d|lpt\d)$/i; | ||
const reControlChars = /[\u0000-\u001F\u0080-\u009F]/g; | ||
const reTrailingPeriods = /\.+$/; | ||
const allowedLength = 50; | ||
function trimRepeated(string: string, target: string) { | ||
const escapeStringRegexp = target | ||
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') | ||
.replace(/-/g, '\\x2d'); | ||
const regex = new RegExp(`(?:${escapeStringRegexp}){2,}`, 'g'); | ||
return string.replace(regex, target); | ||
} | ||
string = string | ||
.normalize('NFD') | ||
.replace(filenameReservedRegex, replacement) | ||
.replace(reControlChars, replacement) | ||
.replace(reTrailingPeriods, ''); | ||
string = trimRepeated(string, replacement); | ||
string = windowsReservedNameRegex.test(string) | ||
? string + replacement | ||
: string; | ||
const extIndex = string.lastIndexOf('.'); | ||
const filename = string.slice(0, extIndex).trim(); | ||
const extension = string.slice(extIndex); | ||
string = | ||
filename.slice(0, Math.max(1, allowedLength - extension.length)) + | ||
extension; | ||
return string; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3561
253304
85
1
+ Added@blocksuite/blocks@0.4.0-20230110000526-9d95ace(transitive)
+ Added@blocksuite/phasor@0.4.0-20230110000526-9d95ace(transitive)
+ Added@blocksuite/store@0.4.0-20230110000526-9d95ace(transitive)
- Removed@blocksuite/blocks@0.3.1(transitive)
- Removed@blocksuite/store@0.3.1(transitive)