mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
Merge branch 'main' into sandboxed
This commit is contained in:
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { ResizableNodeView } from "./resizable-nodeview";
|
||||
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { normalizeFileUrl } from "./media-utils";
|
||||
import { normalizeFileUrl, syncAltBadge } from "./media-utils";
|
||||
|
||||
export type DrawioResizeOptions = {
|
||||
enabled: boolean;
|
||||
@@ -293,6 +293,8 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
const container = nodeView.dom as HTMLElement;
|
||||
applyAlignment(container, align);
|
||||
|
||||
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
|
||||
|
||||
currentNode = updatedNode;
|
||||
return true;
|
||||
},
|
||||
@@ -310,6 +312,8 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
|
||||
const dom = nodeView.dom as HTMLElement;
|
||||
|
||||
syncAltBadge(nodeView.wrapper, node.attrs.alt);
|
||||
|
||||
applyAlignment(dom, node.attrs.align || "center");
|
||||
|
||||
// Handle percentage width backward compat
|
||||
|
||||
@@ -73,9 +73,13 @@ export const embedProviders: IEmbedProvider[] = [
|
||||
id: "vimeo",
|
||||
name: "Vimeo",
|
||||
regex:
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
|
||||
getEmbedUrl: (match) => {
|
||||
return `https://player.vimeo.com/video/${match[4]}`;
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:\/([\da-zA-Z]+))?/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
// preserve ?h= hash for unlisted videos
|
||||
const hash =
|
||||
match[5] ?? new URL(url, "https://vimeo.com").searchParams.get("h");
|
||||
const base = `https://player.vimeo.com/video/${match[4]}`;
|
||||
return hash ? `${base}?h=${hash}` : base;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { ResizableNodeView } from "./resizable-nodeview";
|
||||
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { normalizeFileUrl } from "./media-utils";
|
||||
import { normalizeFileUrl, syncAltBadge } from "./media-utils";
|
||||
|
||||
export type ExcalidrawResizeOptions = {
|
||||
enabled: boolean;
|
||||
@@ -293,6 +293,8 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
const container = nodeView.dom as HTMLElement;
|
||||
applyAlignment(container, align);
|
||||
|
||||
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
|
||||
|
||||
currentNode = updatedNode;
|
||||
return true;
|
||||
},
|
||||
@@ -310,6 +312,8 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
|
||||
const dom = nodeView.dom as HTMLElement;
|
||||
|
||||
syncAltBadge(nodeView.wrapper, node.attrs.alt);
|
||||
|
||||
applyAlignment(dom, node.attrs.align || "center");
|
||||
|
||||
// Handle percentage width backward compat
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@tiptap/core";
|
||||
import { ResizableNodeView } from "../resizable-nodeview";
|
||||
import type { ResizableNodeViewDirection } from "../resizable-nodeview";
|
||||
import { normalizeFileUrl } from "../media-utils";
|
||||
import { normalizeFileUrl, syncAltBadge } from "../media-utils";
|
||||
|
||||
export type ImageResizeOptions = {
|
||||
enabled: boolean;
|
||||
@@ -316,6 +316,8 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
||||
const container = nodeView.dom as HTMLElement;
|
||||
applyAlignment(container, align);
|
||||
|
||||
syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt);
|
||||
|
||||
currentNode = updatedNode;
|
||||
return true;
|
||||
},
|
||||
@@ -333,6 +335,8 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
||||
|
||||
const dom = nodeView.dom as HTMLElement;
|
||||
|
||||
syncAltBadge(nodeView.wrapper, node.attrs.alt);
|
||||
|
||||
// Apply initial alignment
|
||||
applyAlignment(dom, node.attrs.align || "center");
|
||||
|
||||
|
||||
@@ -61,6 +61,11 @@ export function markdownToHtml(
|
||||
.trimStart();
|
||||
|
||||
resetFootnotes();
|
||||
const html = marked.parse(markdown).toString();
|
||||
// marked always closes fenced code with a newline that the editor keeps as
|
||||
// an empty trailing line inside the code block.
|
||||
const html = marked
|
||||
.parse(markdown)
|
||||
.toString()
|
||||
.replace(/\n<\/code><\/pre>/g, "</code></pre>");
|
||||
return html + renderFootnotesList();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,27 @@ export function normalizeFileUrl(src: string): string {
|
||||
return src || "";
|
||||
}
|
||||
|
||||
export function syncAltBadge(wrapper: HTMLElement, alt: unknown): void {
|
||||
const existing = wrapper.querySelector<HTMLElement>(
|
||||
":scope > .media-alt-badge",
|
||||
);
|
||||
|
||||
if (typeof alt !== "string" || !alt.trim()) {
|
||||
existing?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const badge = existing ?? document.createElement("span");
|
||||
badge.dataset.alt = alt;
|
||||
|
||||
if (!existing) {
|
||||
badge.className = "media-alt-badge";
|
||||
badge.textContent = "ALT";
|
||||
badge.setAttribute("aria-hidden", "true");
|
||||
wrapper.appendChild(badge);
|
||||
}
|
||||
}
|
||||
|
||||
export type UploadFn = (
|
||||
file: File,
|
||||
editor: Editor,
|
||||
|
||||
@@ -39,7 +39,11 @@ declare module "@tiptap/core" {
|
||||
/**
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
setSearchTerms: (searchTerms: string[]) => ReturnType;
|
||||
/**
|
||||
* @description Set whole word search in extension.
|
||||
*/
|
||||
setWholeWord: (wholeWord: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
@@ -82,13 +86,26 @@ interface TextNodesWithPosition {
|
||||
}
|
||||
|
||||
const getRegex = (
|
||||
s: string,
|
||||
searchTerms: string[],
|
||||
disableRegex: boolean,
|
||||
caseSensitive: boolean,
|
||||
wholeWord: boolean,
|
||||
): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s,
|
||||
caseSensitive ? "gu" : "gui",
|
||||
const terms = searchTerms.filter(Boolean).sort((a, b) => b.length - a.length);
|
||||
|
||||
const pattern = terms
|
||||
.map((term) =>
|
||||
disableRegex ? term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : term,
|
||||
)
|
||||
.join("|");
|
||||
|
||||
const finalPattern = wholeWord
|
||||
? `(?<![\\p{L}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{N}_])`
|
||||
: pattern;
|
||||
|
||||
return new RegExp(
|
||||
finalPattern,
|
||||
caseSensitive ? "gu" : "giu",
|
||||
);
|
||||
};
|
||||
|
||||
@@ -255,12 +272,14 @@ export interface SearchAndReplaceOptions {
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceStorage {
|
||||
searchTerm: string;
|
||||
searchTerms: string[];
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
lastSearchTerms: string[];
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
lastWholeWord: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
}
|
||||
@@ -280,11 +299,13 @@ export const SearchAndReplace = Extension.create<
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: "",
|
||||
searchTerms: [],
|
||||
replaceTerm: "",
|
||||
results: [],
|
||||
lastSearchTerm: "",
|
||||
lastSearchTerms: [],
|
||||
caseSensitive: false,
|
||||
wholeWord: false,
|
||||
lastWholeWord: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0,
|
||||
@@ -293,10 +314,21 @@ export const SearchAndReplace = Extension.create<
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
setSearchTerms:
|
||||
(searchTerms: string[]) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
editor.storage.searchAndReplace.searchTerms = searchTerms.filter(Boolean);
|
||||
|
||||
// clear whole word by default
|
||||
// should remove if whole word toggle is added to search and replace dialog
|
||||
editor.storage.searchAndReplace.wholeWord = false;
|
||||
|
||||
return false;
|
||||
},
|
||||
setWholeWord:
|
||||
(wholeWord: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.wholeWord = wholeWord;
|
||||
|
||||
return false;
|
||||
},
|
||||
@@ -409,8 +441,10 @@ export const SearchAndReplace = Extension.create<
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
|
||||
const setLastSearchTerm = (t: string) =>
|
||||
(editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastSearchTerms = (terms: string[]) =>
|
||||
(editor.storage.searchAndReplace.lastSearchTerms = [...terms]);
|
||||
const setLastWholeWord = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastWholeWord = t);
|
||||
const setLastCaseSensitive = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) =>
|
||||
@@ -425,37 +459,47 @@ export const SearchAndReplace = Extension.create<
|
||||
const storage = editor.storage.searchAndReplace;
|
||||
if (!storage) return oldState;
|
||||
const {
|
||||
searchTerm,
|
||||
lastSearchTerm,
|
||||
caseSensitive,
|
||||
searchTerms,
|
||||
lastCaseSensitive,
|
||||
lastSearchTerms,
|
||||
caseSensitive,
|
||||
wholeWord,
|
||||
lastWholeWord,
|
||||
resultIndex,
|
||||
lastResultIndex,
|
||||
} = storage;
|
||||
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
searchTerms.length === lastSearchTerms.length &&
|
||||
searchTerms.every((term, index) => term === lastSearchTerms[index]) &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastWholeWord === wholeWord &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastSearchTerms(searchTerms);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastWholeWord(wholeWord);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
if (searchTerms.length === 0) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex,
|
||||
);
|
||||
doc,
|
||||
getRegex(
|
||||
searchTerms,
|
||||
disableRegex,
|
||||
caseSensitive,
|
||||
wholeWord,
|
||||
),
|
||||
searchResultClass,
|
||||
resultIndex,
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user