mirror of
https://github.com/Shadowfita/docmost.git
synced 2026-07-24 00:42:46 +10:00
Merge branch 'main' into Merged-Downstream
# Conflicts: # apps/client/package.json # apps/client/src/features/editor/extensions/extensions.ts # apps/client/src/pages/auth/login.tsx # apps/server/package.json # apps/server/src/integrations/environment/environment.service.ts # packages/editor-ext/src/index.ts # pnpm-lock.yaml
This commit is contained in:
@@ -14,4 +14,6 @@ export * from "./lib/attachment";
|
||||
export * from "./lib/custom-code-block"
|
||||
export * from "./lib/drawio";
|
||||
export * from "./lib/excalidraw";
|
||||
export * from "./lib/embed";
|
||||
export * from "./lib/markdown";
|
||||
export * from "./lib/table-of-contents";
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface CustomCodeBlockOptions extends CodeBlockLowlightOptions {
|
||||
view: any;
|
||||
}
|
||||
|
||||
const TAB_CHAR = "\u00A0\u00A0";
|
||||
|
||||
export const CustomCodeBlock = CodeBlockLowlight.extend<CustomCodeBlockOptions>(
|
||||
{
|
||||
selectable: true,
|
||||
@@ -18,8 +20,26 @@ export const CustomCodeBlock = CodeBlockLowlight.extend<CustomCodeBlockOptions>(
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
Tab: () => {
|
||||
if (this.editor.isActive("codeBlock")) {
|
||||
this.editor
|
||||
.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.insertText(TAB_CHAR);
|
||||
return true;
|
||||
})
|
||||
.run();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
|
||||
export interface EmbedOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
view: any;
|
||||
}
|
||||
export interface EmbedAttributes {
|
||||
src?: string;
|
||||
provider: string;
|
||||
align?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
embeds: {
|
||||
setEmbed: (attributes?: EmbedAttributes) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Embed = Node.create<EmbedOptions>({
|
||||
name: 'embed',
|
||||
inline: false,
|
||||
group: 'block',
|
||||
isolating: true,
|
||||
atom: true,
|
||||
defining: true,
|
||||
draggable: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
view: null,
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: '',
|
||||
parseHTML: (element) => element.getAttribute('data-src'),
|
||||
renderHTML: (attributes: EmbedAttributes) => ({
|
||||
'data-src': attributes.src,
|
||||
}),
|
||||
},
|
||||
provider: {
|
||||
default: '',
|
||||
parseHTML: (element) => element.getAttribute('data-provider'),
|
||||
renderHTML: (attributes: EmbedAttributes) => ({
|
||||
'data-provider': attributes.provider,
|
||||
}),
|
||||
},
|
||||
align: {
|
||||
default: 'center',
|
||||
parseHTML: (element) => element.getAttribute('data-align'),
|
||||
renderHTML: (attributes: EmbedAttributes) => ({
|
||||
'data-align': attributes.align,
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
default: 640,
|
||||
parseHTML: (element) => element.getAttribute('data-width'),
|
||||
renderHTML: (attributes: EmbedAttributes) => ({
|
||||
'data-width': attributes.width,
|
||||
}),
|
||||
},
|
||||
height: {
|
||||
default: 480,
|
||||
parseHTML: (element) => element.getAttribute('data-height'),
|
||||
renderHTML: (attributes: EmbedAttributes) => ({
|
||||
'data-height': attributes.height,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(
|
||||
{ "data-type": this.name },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
),
|
||||
[
|
||||
"a",
|
||||
{
|
||||
href: HTMLAttributes["data-src"],
|
||||
target: "blank",
|
||||
},
|
||||
`${HTMLAttributes["data-src"]}`,
|
||||
],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setEmbed:
|
||||
(attrs: EmbedAttributes) =>
|
||||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: 'embed',
|
||||
attrs: attrs,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
});
|
||||
@@ -10,11 +10,35 @@ export const LinkExtension = TiptapLink.extend({
|
||||
return [
|
||||
{
|
||||
tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])',
|
||||
getAttrs: (element) => {
|
||||
if (
|
||||
element
|
||||
.getAttribute("href")
|
||||
?.toLowerCase()
|
||||
.startsWith("javascript:")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
if (HTMLAttributes.href?.toLowerCase().startsWith("javascript:")) {
|
||||
return [
|
||||
"a",
|
||||
mergeAttributes(
|
||||
this.options.HTMLAttributes,
|
||||
{ ...HTMLAttributes, href: "" },
|
||||
{ class: "link" },
|
||||
),
|
||||
0,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"a",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./utils/marked.utils";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Token, marked } from 'marked';
|
||||
|
||||
interface CalloutToken {
|
||||
type: 'callout';
|
||||
calloutType: string;
|
||||
text: string;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export const calloutExtension = {
|
||||
name: 'callout',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/:::/)?.index ?? -1;
|
||||
},
|
||||
tokenizer(src: string): CalloutToken | undefined {
|
||||
const rule = /^:::([a-zA-Z0-9]+)\s+([\s\S]+?):::/;
|
||||
const match = rule.exec(src);
|
||||
|
||||
const validCalloutTypes = ['info', 'success', 'warning', 'danger'];
|
||||
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
if (!validCalloutTypes.includes(type)) {
|
||||
type = 'info';
|
||||
}
|
||||
return {
|
||||
type: 'callout',
|
||||
calloutType: type,
|
||||
raw: match[0],
|
||||
text: match[2].trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Token) {
|
||||
const calloutToken = token as CalloutToken;
|
||||
const body = marked.parse(calloutToken.text);
|
||||
|
||||
return `<div data-type="callout" data-callout-type="${calloutToken.calloutType}">${body}</div>`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { marked } from 'marked';
|
||||
import { calloutExtension } from './callout.marked';
|
||||
import { mathBlockExtension } from './math-block.marked';
|
||||
import { mathInlineExtension } from "./math-inline.marked";
|
||||
|
||||
marked.use({
|
||||
renderer: {
|
||||
// @ts-ignore
|
||||
list(body: string, isOrdered: boolean, start: number) {
|
||||
if (isOrdered) {
|
||||
const startAttr = start !== 1 ? ` start="${start}"` : '';
|
||||
return `<ol ${startAttr}>\n${body}</ol>\n`;
|
||||
}
|
||||
|
||||
const dataType = body.includes(`<input`) ? ' data-type="taskList"' : '';
|
||||
return `<ul${dataType}>\n${body}</ul>\n`;
|
||||
},
|
||||
// @ts-ignore
|
||||
listitem({ text, raw, task: isTask, checked: isChecked }): string {
|
||||
if (!isTask) {
|
||||
return `<li>${text}</li>\n`;
|
||||
}
|
||||
const checkedAttr = isChecked
|
||||
? 'data-checked="true"'
|
||||
: 'data-checked="false"';
|
||||
return `<li data-type="taskItem" ${checkedAttr}>${text}</li>\n`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
marked.use({ extensions: [calloutExtension, mathBlockExtension, mathInlineExtension] });
|
||||
|
||||
export function markdownToHtml(markdownInput: string): string | Promise<string> {
|
||||
const YAML_FONT_MATTER_REGEX = /^\s*---[\s\S]*?---\s*/;
|
||||
|
||||
const markdown = markdownInput
|
||||
.replace(YAML_FONT_MATTER_REGEX, '')
|
||||
.trimStart();
|
||||
|
||||
return marked.parse(markdown);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Token, marked } from 'marked';
|
||||
|
||||
interface MathBlockToken {
|
||||
type: 'mathBlock';
|
||||
text: string;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export const mathBlockExtension = {
|
||||
name: 'mathBlock',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/\$\$/)?.index ?? -1;
|
||||
},
|
||||
tokenizer(src: string): MathBlockToken | undefined {
|
||||
const rule = /^\$\$(?!(\$))([\s\S]+?)\$\$/;
|
||||
const match = rule.exec(src);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
type: 'mathBlock',
|
||||
raw: match[0],
|
||||
text: match[2]?.trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Token) {
|
||||
const mathBlockToken = token as MathBlockToken;
|
||||
// parse to prevent escaping slashes
|
||||
const latex = marked
|
||||
.parse(mathBlockToken.text)
|
||||
.toString()
|
||||
.replace(/<(\/)?p>/g, '');
|
||||
|
||||
return `<div data-type="${mathBlockToken.type}" data-katex="true">${latex}</div>`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Token, marked } from 'marked';
|
||||
|
||||
interface MathInlineToken {
|
||||
type: 'mathInline';
|
||||
text: string;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
const inlineMathRegex = /^\$(?!\s)(.+?)(?<!\s)\$(?!\d)/;
|
||||
|
||||
export const mathInlineExtension = {
|
||||
name: 'mathInline',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index: number;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('$');
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const f = index === 0 || indexSrc.charAt(index - 1) === ' ';
|
||||
if (f) {
|
||||
const possibleKatex = indexSrc.substring(index);
|
||||
if (possibleKatex.match(inlineMathRegex)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, '');
|
||||
}
|
||||
},
|
||||
tokenizer(src: string): MathInlineToken | undefined {
|
||||
const match = inlineMathRegex.exec(src);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
type: 'mathInline',
|
||||
raw: match[0],
|
||||
text: match[1]?.trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Token) {
|
||||
const mathInlineToken = token as MathInlineToken;
|
||||
// parse to prevent escaping slashes
|
||||
const latex = marked
|
||||
.parse(mathInlineToken.text)
|
||||
.toString()
|
||||
.replace(/<(\/)?p>/g, '');
|
||||
|
||||
return `<span data-type="${mathInlineToken.type}" data-katex="true">${latex}</span>`;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user