diff --git a/docs/.ruby-version b/docs/.ruby-version index 3a8d0124..7bde84d0 100644 --- a/docs/.ruby-version +++ b/docs/.ruby-version @@ -1 +1 @@ -ruby-3.0.4 \ No newline at end of file +ruby-3.1.2 diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index a6a6e852..0758d4cd 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -133,6 +133,7 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES diff --git a/tests/rails/Gemfile.lock b/tests/rails/Gemfile.lock index a7e0b421..a2a2e0c2 100644 --- a/tests/rails/Gemfile.lock +++ b/tests/rails/Gemfile.lock @@ -276,7 +276,7 @@ GEM y-rb_actioncable (0.1.7) rails (>= 7.0.4) y-rb (>= 0.4.5) - zeitwerk (2.7.1) + zeitwerk (2.6.18) PLATFORMS arm64-darwin-21 diff --git a/tests/rails/app/frontend/controllers/markdown_link.js b/tests/rails/app/frontend/controllers/markdown_link.js new file mode 100644 index 00000000..48f90c3c --- /dev/null +++ b/tests/rails/app/frontend/controllers/markdown_link.js @@ -0,0 +1,118 @@ +/** + * Adapted from Typist's extension: https://github.com/Doist/typist/blob/main/src/extensions/rich-text/rich-text-link.ts + */ +import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core' +import { Link } from '@tiptap/extension-link' +// import type { LinkOptions } from '@tiptap/extension-link' + +/** + * The input regex for Markdown links with title support, and multiple quotation marks (required + * in case the `Typography` extension is being included). + */ +const inputRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)$/i + +/** + * The paste regex for Markdown links with title support, and multiple quotation marks (required + * in case the `Typography` extension is being included). + */ +const pasteRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)/gi + +/** + * Input rule built specifically for the `Link` extension, which ignores the auto-linked URL in + * parentheses (e.g., `(https://doist.dev)`). + * + * @see https://github.com/ueberdosis/tiptap/discussions/1865 + */ +function linkInputRule(config) { + const defaultMarkInputRule = markInputRule(config) + + return new InputRule({ + find: config.find, + handler(props) { + const { tr } = props.state + + defaultMarkInputRule.handler(props) + tr.setMeta('preventAutolink', true) + }, + }) +} + +/** + * Paste rule built specifically for the `Link` extension, which ignores the auto-linked URL in + * parentheses (e.g., `(https://doist.dev)`). This extension was inspired from the multiple + * implementations found in a Tiptap discussion at GitHub. + * + * @see https://github.com/ueberdosis/tiptap/discussions/1865 + */ +function linkPasteRule(config) { + const defaultMarkPasteRule = markPasteRule(config) + + return new PasteRule({ + find: config.find, + handler(props) { + const { tr } = props.state + + defaultMarkPasteRule.handler(props) + tr.setMeta('preventAutolink', true) + }, + }) +} + +/** + * Custom extension that extends the built-in `Link` extension to add additional input/paste rules + * for converting the Markdown link syntax (i.e. `[Doist](https://doist.com)`) into links, and also + * adds support for the `title` attribute. + */ +export const MarkdownLink = Link.extend({ + inclusive: false, + addOptions() { + return { + ...this.parent?.(), + openOnClick: 'whenNotEditable', + } + }, + addAttributes() { + return { + ...this.parent?.(), + title: { + default: null, + }, + } + }, + addInputRules() { + return [ + linkInputRule({ + find: inputRegex, + type: this.type, + + // We need to use `pop()` to remove the last capture groups from the match to + // satisfy Tiptap's `markPasteRule` expectation of having the content as the last + // capture group in the match (this makes the attribute order important) + getAttributes(match) { + return { + title: match.pop()?.trim(), + href: match.pop()?.trim(), + } + }, + }), + ] + }, + addPasteRules() { + return [ + linkPasteRule({ + find: pasteRegex, + type: this.type, + + // We need to use `pop()` to remove the last capture groups from the match to + // satisfy Tiptap's `markInputRule` expectation of having the content as the last + // capture group in the match (this makes the attribute order important) + getAttributes(match) { + return { + title: match.pop()?.trim(), + href: match.pop()?.trim(), + } + }, + }), + ] + }, +}) diff --git a/tests/rails/app/frontend/entrypoints/application.js b/tests/rails/app/frontend/entrypoints/application.js index 3d1dd230..9cb2b622 100644 --- a/tests/rails/app/frontend/entrypoints/application.js +++ b/tests/rails/app/frontend/entrypoints/application.js @@ -8,6 +8,7 @@ import "trix/dist/trix.css"; import { Application } from "@hotwired/stimulus" import EmbedController from "../controllers/embed_controller.js" import TipTapMirrorController from "../controllers/tip_tap_mirror_controller.js" +import { MarkdownLink } from "../controllers/markdown_link.js" window.Stimulus = Application.start() window.Stimulus.debug = true Stimulus.register("embed", EmbedController) @@ -32,6 +33,11 @@ ActiveStorage.start() if (trixHtmlMirror) Prism.highlightElement(trixHtmlMirror) if (tipTapHtmlMirror) Prism.highlightElement(tipTapHtmlMirror) + const rhinoEditor = document.querySelector("rhino-editor[input=y]") + rhinoEditor.addExtensions( + MarkdownLink, + ) + const escapeHTML = (str) => { const p = document.createElement("p"); p.appendChild(document.createTextNode(str));