add markdown editor

pull/601/head
Alex Turchyn 4 weeks ago committed by Pete Matsyburka
parent 451f83421a
commit 39cc82ce0c

@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor'
import MarkdownEditor from './elements/markdown_editor'
import MountOnClick from './elements/mount_on_click'
import RemoveOnEvent from './elements/remove_on_event'
import ScrollTo from './elements/scroll_to'
@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('markdown-editor', MarkdownEditor)
safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('remove-on-event', RemoveOnEvent)
safeRegisterElement('scroll-to', ScrollTo)

@ -0,0 +1,411 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
function loadTiptap () {
return Promise.all([
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
Editor: core.Editor,
Extension: core.Extension,
Bold: bold.default || bold,
Italic: italic.default || italic,
Paragraph: paragraph.default || paragraph,
Text: text.default || text,
HardBreak: hardBreak.default || hardBreak,
Document: document.default || document,
Link: link.default || link,
Underline: underline.default || underline,
UndoRedo: extensions.UndoRedo,
Markdown: markdown.Markdown,
Plugin: pmState.Plugin,
Decoration: pmView.Decoration,
DecorationSet: pmView.DecorationSet
}))
}
class LinkTooltip {
constructor (tooltip, input, saveButton, removeButton, normalizeUrlFn) {
this.tooltip = tooltip
this.input = input
this.saveButton = saveButton
this.removeButton = removeButton
this.normalizeUrl = normalizeUrlFn
this.targetElement = null
this.clickOutsideTimeout = null
this.closeOnClickOutside = null
this.scrollHandler = null
this.saveHandler = null
this.removeHandler = null
this.keyHandler = null
}
updatePosition () {
const rect = this.targetElement.getBoundingClientRect()
this.tooltip.style.left = `${rect.left}px`
this.tooltip.style.top = `${rect.bottom + 5}px`
}
setupClickOutside () {
this.closeOnClickOutside = (e) => {
if (!this.tooltip.contains(e.target)) {
this.hide()
}
}
this.clickOutsideTimeout = setTimeout(() => {
if (this.closeOnClickOutside) {
document.addEventListener('click', this.closeOnClickOutside)
}
}, 100)
}
setupScrollTracking () {
this.scrollHandler = () => this.updatePosition()
window.addEventListener('scroll', this.scrollHandler, true)
}
show ({ url, targetElement, onSave, onRemove }) {
this.hide()
this.input.value = url || ''
this.removeButton.classList.toggle('hidden', !url)
this.targetElement = targetElement
this.updatePosition()
this.tooltip.classList.remove('hidden')
this.input.focus()
this.input.select()
const save = () => {
const inputUrl = this.input.value.trim()
this.hide()
if (inputUrl) {
onSave(this.normalizeUrl(inputUrl))
}
}
this.saveHandler = () => save()
this.removeHandler = () => {
if (onRemove) onRemove()
this.hide()
}
this.keyHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault()
save()
} else if (e.key === 'Escape') {
e.preventDefault()
this.hide()
}
}
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
this.input.addEventListener('keydown', this.keyHandler)
this.setupScrollTracking()
this.setupClickOutside()
}
hide () {
if (this.clickOutsideTimeout) {
clearTimeout(this.clickOutsideTimeout)
this.clickOutsideTimeout = null
}
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler, true)
this.scrollHandler = null
}
if (this.closeOnClickOutside) {
document.removeEventListener('click', this.closeOnClickOutside)
this.closeOnClickOutside = null
}
if (this.saveHandler) {
this.saveButton.removeEventListener('click', this.saveHandler)
this.saveHandler = null
}
if (this.removeHandler) {
this.removeButton.removeEventListener('click', this.removeHandler)
this.removeHandler = null
}
if (this.keyHandler) {
this.input.removeEventListener('keydown', this.keyHandler)
this.keyHandler = null
}
this.tooltip?.classList.add('hidden')
this.targetElement = null
}
}
export default targetable(class extends HTMLElement {
static [target.static] = [
'textarea',
'editorElement',
'variableButton',
'variableDropdown',
'boldButton',
'italicButton',
'underlineButton',
'linkButton',
'undoButton',
'redoButton',
'linkTooltipElement',
'linkInput',
'linkSaveButton',
'linkRemoveButton'
]
async connectedCallback () {
if (!this.textarea || !this.editorElement) return
this.textarea.style.display = 'none'
this.adjustShortcutsForPlatform()
this.linkTooltip = new LinkTooltip(
this.linkTooltipElement,
this.linkInput,
this.linkSaveButton,
this.linkRemoveButton,
(url) => this.normalizeUrl(url)
)
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{[a-zA-Z0-9_.-]+\}/g
doc.descendants((node, pos) => {
if (!node.isText) return
let match
while ((match = regex.exec(node.text)) !== null) {
decorations.push(
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
class: 'bg-amber-100 py-0.5 px-1 rounded'
})
)
}
})
return DecorationSet.create(doc, decorations)
}
const VariableHighlight = Extension.create({
name: 'variableHighlight',
addProseMirrorPlugins () {
return [new Plugin({
state: {
init (_, { doc }) {
return buildDecorations(doc)
},
apply (tr, oldSet) {
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
}
},
props: {
decorations (state) {
return this.getState(state)
}
}
})]
}
})
this.editor = new Editor({
element: this.editorElement,
extensions: [
Markdown,
Document,
Paragraph,
Text,
Bold,
Italic,
HardBreak,
UndoRedo,
Link.extend({
inclusive: false
}).configure({
openOnClick: false,
HTMLAttributes: {
class: 'link',
style: 'color: #2563eb; text-decoration: underline; cursor: pointer;'
}
}),
Underline,
VariableHighlight
],
content: this.textarea.value || '',
contentType: 'markdown',
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none p-3 outline-none focus:outline-none min-h-[120px]'
},
handleClick: (view, pos, event) => {
const clickedPos = view.posAtCoords({ left: event.clientX, top: event.clientY })
if (!clickedPos) return false
const linkMark = view.state.doc.resolve(clickedPos.pos).marks().find(m => m.type.name === 'link')
if (linkMark) {
event.preventDefault()
this.editor.chain().setTextSelection(clickedPos.pos).extendMarkRange('link').run()
this.toggleLink()
return true
}
return false
}
},
onUpdate: ({ editor }) => {
this.textarea.value = editor.getMarkdown()
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
},
onSelectionUpdate: () => {
this.updateToolbarState()
}
})
this.setupToolbar()
if (this.variableButton) {
this.variableButton.addEventListener('click', () => {
this.variableDropdown.classList.toggle('hidden')
})
this.variableDropdown.addEventListener('click', (e) => {
const variable = e.target.closest('[data-variable]')?.dataset.variable
if (variable) {
this.insertVariable(variable)
this.variableDropdown.classList.add('hidden')
}
})
document.addEventListener('click', (e) => {
if (!this.variableButton.contains(e.target) && !this.variableDropdown.contains(e.target)) {
this.variableDropdown.classList.add('hidden')
}
})
}
}
adjustShortcutsForPlatform () {
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
const tip = tooltip.getAttribute('data-tip')
if (tip && tip.includes('Ctrl')) {
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
}
})
}
}
setupToolbar () {
this.boldButton?.addEventListener('click', (e) => {
e.preventDefault()
this.editor.chain().focus().toggleBold().run()
this.updateToolbarState()
})
this.italicButton?.addEventListener('click', (e) => {
e.preventDefault()
this.editor.chain().focus().toggleItalic().run()
this.updateToolbarState()
})
this.underlineButton?.addEventListener('click', (e) => {
e.preventDefault()
this.editor.chain().focus().toggleUnderline().run()
this.updateToolbarState()
})
this.linkButton?.addEventListener('click', (e) => {
e.preventDefault()
this.toggleLink()
})
this.undoButton?.addEventListener('click', (e) => {
e.preventDefault()
this.editor.chain().focus().undo().run()
})
this.redoButton?.addEventListener('click', (e) => {
e.preventDefault()
this.editor.chain().focus().redo().run()
})
}
updateToolbarState () {
this.boldButton?.classList.toggle('bg-base-200', this.editor.isActive('bold'))
this.italicButton?.classList.toggle('bg-base-200', this.editor.isActive('italic'))
this.underlineButton?.classList.toggle('bg-base-200', this.editor.isActive('underline'))
this.linkButton?.classList.toggle('bg-base-200', this.editor.isActive('link'))
}
normalizeUrl (url) {
if (!url) return url
if (/^https?:\/\//i.test(url)) return url
if (/^mailto:/i.test(url)) return url
return `https://${url}`
}
toggleLink () {
const { from } = this.editor.state.selection
const rect = this.editor.view.coordsAtPos(from)
const fakeElement = { getBoundingClientRect: () => rect }
const previousUrl = this.editor.getAttributes('link').href
this.linkTooltip.show({
url: previousUrl,
targetElement: fakeElement,
onSave: (url) => {
this.editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
},
onRemove: previousUrl
? () => { this.editor.chain().focus().extendMarkRange('link').unsetLink().run() }
: null
})
}
insertVariable (variable) {
this.editor.chain().focus().insertContent(`{${variable}}`).run()
}
disconnectedCallback () {
this.linkTooltip.hide()
if (this.editor) {
this.editor.destroy()
this.editor = null
}
}
})

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 14l-4 -4l4 -4" />
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 14l4 -4l-4 -4" />
<path d="M19 10h-11a4 4 0 1 0 0 8h1" />
</svg>

After

Width:  |  Height:  |  Size: 356 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6l0 -7" />
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

@ -0,0 +1,6 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 5l6 0" />
<path d="M7 19l6 0" />
<path d="M14 5l-4 14" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5v5a5 5 0 0 0 10 0v-5" />
<path d="M5 19h14" />
</svg>

After

Width:  |  Height:  |  Size: 345 B

@ -1,11 +1,5 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
<% variables = AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/\{([^}]+)\}/).flatten.uniq %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:config].value['body'], variables: variables %>
</div>

@ -0,0 +1,68 @@
<markdown-editor>
<div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'code' => t('variables.code'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="ml-auto relative">
<button type="button" data-target="markdown-editor.variableButton" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</button>
<div data-target="markdown-editor.variableDropdown" class="hidden absolute right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<div data-target="markdown-editor.editorElement"></div>
</div>
<div data-target="markdown-editor.linkTooltipElement" class="hidden fixed flex bg-white border border-base-300 rounded-xl shadow-lg p-1 gap-1 items-center z-50">
<input data-target="markdown-editor.linkInput" type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 w-80 text-sm outline-none" autocomplete="off">
<button type="button" data-target="markdown-editor.linkSaveButton" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-target="markdown-editor.linkRemoveButton" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor>

@ -55,15 +55,9 @@
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= f.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% body_variables = AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/\{([^}]+)\}/).flatten.uniq %>
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
@ -97,15 +91,8 @@
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
</div>
</div>
<% end %>

@ -887,6 +887,13 @@ en: &en
if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one."
too_many_requests_try_again_later: Too many requests, try again later.
bold: Bold
italic: Italic
underline: Underline
undo: Undo
redo: Redo
add_variable: Add variable
enter_a_url_or_variable_name: Enter a URL or variable name
devise:
confirmations:
confirmed: Your email address has been successfully confirmed.
@ -1003,6 +1010,13 @@ en: &en
events:
range_with_total: "%{from}-%{to} of %{count} events"
range_without_total: "%{from}-%{to} events"
variables:
account_name: Account name
submitter_link: Submitter link
template_name: Template name
submission_submitters: Submission submitters
submission_link: Submission link
documents_link: Documents link
time:
formats:
detailed: "%B %d, %Y %H:%M:%S"
@ -1875,6 +1889,13 @@ es: &es
if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva."
too_many_requests_try_again_later: Demasiadas solicitudes. Intenta de nuevo más tarde.
bold: Negrita
italic: Cursiva
underline: Subrayado
undo: Deshacer
redo: Rehacer
add_variable: Agregar variable
enter_a_url_or_variable_name: Ingrese una URL o nombre de variable
devise:
confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@ -1991,6 +2012,13 @@ es: &es
events:
range_with_total: "%{from}-%{to} de %{count} eventos"
range_without_total: "%{from}-%{to} eventos"
variables:
account_name: Nombre de la cuenta
submitter_link: Enlace del firmante
template_name: Nombre de la plantilla
submission_submitters: Firmantes del envío
submission_link: Enlace del envío
documents_link: Enlace de los documentos
time:
formats:
detailed: "%-d de %B de %Y %H:%M:%S"
@ -2864,6 +2892,13 @@ it: &it
if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova."
too_many_requests_try_again_later: Troppe richieste. Riprova più tardi.
bold: Grassetto
italic: Corsivo
underline: Sottolineato
undo: Annulla
redo: Ripeti
add_variable: Aggiungi variabile
enter_a_url_or_variable_name: Inserisci un URL o nome variabile
devise:
confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo.
@ -2980,6 +3015,13 @@ it: &it
events:
range_with_total: "%{from}-%{to} di %{count} eventi"
range_without_total: "%{from}-%{to} eventi"
variables:
account_name: Nome dell'account
submitter_link: Link del firmatario
template_name: Nome del modello
submission_submitters: Firmatari dell'invio
submission_link: Link dell'invio
documents_link: Link dei documenti
time:
formats:
detailed: "%d %B %Y %H:%M:%S"
@ -3849,6 +3891,13 @@ fr: &fr
if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous naurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe."
too_many_requests_try_again_later: Trop de demandes. Réessayez plus tard.
bold: Gras
italic: Italique
underline: Souligné
undo: Annuler
redo: Rétablir
add_variable: Ajouter une variable
enter_a_url_or_variable_name: Entrez une URL ou un nom de variable
devise:
confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès.
@ -3965,6 +4014,13 @@ fr: &fr
events:
range_with_total: "%{from}-%{to} sur %{count} événements"
range_without_total: "%{from}-%{to} événements"
variables:
account_name: Nom du compte
submitter_link: Lien du signataire
template_name: Nom du modèle
submission_submitters: Signataires de la soumission
submission_link: Lien de la soumission
documents_link: Lien des documents
time:
formats:
detailed: "%A %d %B %Y %Hh%Mm%Ss"
@ -4837,6 +4893,13 @@ pt: &pt
if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova."
too_many_requests_try_again_later: Muitas solicitações. Tente novamente mais tarde.
bold: Negrito
italic: Itálico
underline: Sublinhado
undo: Desfazer
redo: Refazer
add_variable: Adicionar variável
enter_a_url_or_variable_name: Digite uma URL ou nome de variável
devise:
confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@ -4953,6 +5016,13 @@ pt: &pt
events:
range_with_total: "%{from}-%{to} de %{count} eventos"
range_without_total: "%{from}-%{to} eventos"
variables:
account_name: Nome da conta
submitter_link: Link do signatário
template_name: Nome do modelo
submission_submitters: Signatários da submissão
submission_link: Link da submissão
documents_link: Link dos documentos
time:
formats:
detailed: "%A, %d de %B de %Y, %H:%M:%Sh"
@ -5825,6 +5895,13 @@ de: &de
if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen."
too_many_requests_try_again_later: Zu viele Anfragen. Versuchen Sie es später erneut.
bold: Fett
italic: Kursiv
underline: Unterstrichen
undo: Rückgängig
redo: Wiederholen
add_variable: Variable hinzufügen
enter_a_url_or_variable_name: Geben Sie eine URL oder einen Variablennamen ein
devise:
confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@ -5941,6 +6018,13 @@ de: &de
events:
range_with_total: "%{from}-%{to} von %{count} Ereignissen"
range_without_total: "%{from}-%{to} Ereignisse"
variables:
account_name: Kontoname
submitter_link: Link des Unterzeichners
template_name: Vorlagenname
submission_submitters: Unterzeichner der Einreichung
submission_link: Link der Einreichung
documents_link: Link der Dokumente
time:
formats:
detailed: "%A, %d. %B %Y, %H:%M:%S Uhr"
@ -7198,6 +7282,13 @@ nl: &nl
if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt."
too_many_requests_try_again_later: Te veel verzoeken. Probeer het later opnieuw.
bold: Vet
italic: Cursief
underline: Onderstreept
undo: Ongedaan maken
redo: Opnieuw
add_variable: Variabele toevoegen
enter_a_url_or_variable_name: Voer een URL of variabelenaam in
devise:
confirmations:
confirmed: Je e-mailadres is succesvol bevestigd.
@ -7314,6 +7405,13 @@ nl: &nl
events:
range_with_total: "%{from}-%{to} van %{count} gebeurtenissen"
range_without_total: "%{from}-%{to} gebeurtenissen"
variables:
account_name: Accountnaam
submitter_link: Link van de ondertekenaar
template_name: Sjabloonnaam
submission_submitters: Ondertekenaars van de inzending
submission_link: Link van de inzending
documents_link: Link van de documenten
time:
formats:
detailed: "%d %B %Y %H:%M:%S"

@ -1,13 +1,115 @@
# frozen_string_literal: true
module MarkdownToHtml
LINK_REGEXP = %r{\[([^\]]+)\]\((https?://[^)]+)\)}
TAGS = {
'' => %w[<em> </em>],
'*' => %w[<strong> </strong>],
'~' => %w[<s> </s>]
}.freeze
INLINE_TOKENIZER = /(\[)|(\]\(([^)]+?)\))|(?:`([^`].*?)`)|(\*\*\*|\*\*|\*|~~)/
ALLOWED_TAGS = %w[p br strong b em i u a].freeze
ALLOWED_ATTRIBUTES = %w[href].freeze
module_function
def call(text)
text.gsub(LINK_REGEXP) do
ApplicationController.helpers.link_to(Regexp.last_match(1), Regexp.last_match(2))
def call(markdown)
return '' if markdown.blank?
text = auto_link_urls(markdown)
html = render_markdown(text)
ActionController::Base.helpers.sanitize(html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
end
def auto_link_urls(text)
link_parts = text.split(%r{((?:https?://|www\.)[^\s)]+)})
link_parts.map.with_index do |part, index|
if part.match?(%r{\A(?:https?://|www\.)}) &&
!link_parts[index - 1]&.match?(/\]\(\s*\z/)
trail = part.match(/([.,;:!?]+)\z/)[1] if part.match?(/[.,;:!?]+\z/)
clean = trail ? part.chomp(trail) : part
url = clean.start_with?('www.') ? "http://#{clean}" : clean
"[#{clean}](#{url})#{trail}"
else
part
end
end.join
end
def render_markdown(text)
text = text.gsub(/\+\+([^+]+)\+\+/, '<u>\1</u>')
paragraphs = text.split(/\n{2,}/)
html = paragraphs.filter_map do |para|
content = para.strip
next if content.empty?
next '<p><br></p>' if ['&nbsp;', '&amp;nbsp;'].include?(content)
content = content.gsub(/ *\n/, '<br>')
"<p>#{parse_inline(content)}</p>"
end.join
html.presence || '<p></p>'
end
# rubocop:disable Metrics
def parse_inline(text)
context = []
out = ''
last = 0
tag = lambda do |t|
desc = TAGS[t[1] || '']
return t unless desc
return desc[0] unless desc[1]
is_end = context.last == t
is_end ? context.pop : context.push(t)
desc[is_end ? 1 : 0]
end
flush = lambda do
str = ''
str += tag.call(context.last) while context.any?
str
end
while last <= text.length && (m = INLINE_TOKENIZER.match(text, last))
prev = text[last...m.begin(0)]
last = m.end(0)
chunk = m[0]
if m[4]
chunk = "<code>#{ERB::Util.html_escape(m[4])}</code>"
elsif m[2]
out = out.sub(/\A(.*)<a>/m, "\\1<a href=\"#{ERB::Util.html_escape(m[3])}\">")
out = out.gsub('<a>', '[')
chunk = "#{flush.call}</a>"
elsif m[1]
chunk = '<a>'
elsif m[5]
chunk = if m[5] == '***'
if context.include?('*') && context.include?('**')
tag.call('*') + tag.call('**')
else
tag.call('**') + tag.call('*')
end
else
tag.call(m[5])
end
end
out += prev.to_s + chunk
end
(out + text[last..].to_s + flush.call).gsub('<a>', '[')
end
# rubocop:enable Metrics
end

@ -14,6 +14,18 @@
"@hotwired/turbo-rails": "^7.3.0",
"@specious/htmlflow": "^1.1.0",
"@tabler/icons-vue": "^2.47.0",
"@tiptap/core": "^3.19.0",
"@tiptap/extension-bold": "^3.19.0",
"@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-hard-break": "^3.19.0",
"@tiptap/extension-italic": "^3.19.0",
"@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-paragraph": "^3.19.0",
"@tiptap/extension-text": "^3.19.0",
"@tiptap/extension-underline": "^3.19.0",
"@tiptap/extensions": "^3.19.0",
"@tiptap/markdown": "^3.19.0",
"@tiptap/pm": "^3.19.0",
"autocompleter": "^9.1.0",
"autoprefixer": "^10.4.14",
"babel-loader": "9.1.2",

@ -1743,6 +1743,11 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41"
integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ==
"@remirror/core-constants@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
"@sinclair/typebox@^0.25.16":
version "0.25.24"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
@ -1767,6 +1772,89 @@
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.47.0.tgz#c41c680d1947e3ab2d60af3febc4132287c60596"
integrity sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==
"@tiptap/core@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.19.0.tgz#dca483b50e1b8a596f695aecde387a79fe7da717"
integrity sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==
"@tiptap/extension-bold@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.19.0.tgz#ef0ddfd9b242ef9c25e3348aef9bf2dc681cdc19"
integrity sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q==
"@tiptap/extension-document@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.19.0.tgz#dfa6889cff748d489e0bc1028918bf4571372ba5"
integrity sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A==
"@tiptap/extension-hard-break@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8"
integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==
"@tiptap/extension-italic@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8"
integrity sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA==
"@tiptap/extension-link@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.19.0.tgz#e8e656735bda6ca1d4b6577821e06274ab0ff6c8"
integrity sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==
dependencies:
linkifyjs "^4.3.2"
"@tiptap/extension-paragraph@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz#91adde189aabf13a2bfbb2d961833d3bc2bc055f"
integrity sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg==
"@tiptap/extension-text@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.19.0.tgz#353278c97bd8f5bdc29f06942fbd1e856bdb5b18"
integrity sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g==
"@tiptap/extension-underline@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz#bbc81d085725981d256127ab416f91d0802ec2a4"
integrity sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw==
"@tiptap/extensions@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.19.0.tgz#5747c0ebf460b9669e8b4362561872448f66abfe"
integrity sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==
"@tiptap/markdown@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/markdown/-/markdown-3.19.0.tgz#dd05451b40f2a553cab0fdbb4a8714a2b2430b5c"
integrity sha512-Pnfacq2FHky1rqwmGwEmUJxuZu8VZ8XjaJIqsQC34S3CQWiOU+PukC9In2odzcooiVncLWT9s97jKuYpbmF1tQ==
dependencies:
marked "^17.0.1"
"@tiptap/pm@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.19.0.tgz#5cb499c7b2603ec6550d0c7a70b924f27fdb7692"
integrity sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
prosemirror-commands "^1.6.2"
prosemirror-dropcursor "^1.8.1"
prosemirror-gapcursor "^1.3.2"
prosemirror-history "^1.4.1"
prosemirror-inputrules "^1.4.0"
prosemirror-keymap "^1.2.2"
prosemirror-markdown "^1.13.1"
prosemirror-menu "^1.2.4"
prosemirror-model "^1.24.1"
prosemirror-schema-basic "^1.2.3"
prosemirror-schema-list "^1.5.0"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.4"
prosemirror-trailing-node "^3.0.0"
prosemirror-transform "^1.10.2"
prosemirror-view "^1.38.1"
"@trysound/sax@0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@ -1909,6 +1997,24 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/linkify-it@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/markdown-it@^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdurl@^2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
@ -2986,7 +3092,7 @@ cosmiconfig@^8.1.3:
parse-json "^5.0.0"
path-type "^4.0.0"
crelt@^1.0.5, crelt@^1.0.6:
crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
@ -4780,6 +4886,18 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^2.0.0"
linkifyjs@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
loader-runner@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
@ -4867,6 +4985,23 @@ make-dir@^3.0.2:
dependencies:
semver "^6.0.0"
markdown-it@^14.0.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
dependencies:
argparse "^2.0.1"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.1.0"
marked@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.1.tgz#9db34197ac145e5929572ee49ef701e37ee9b2e6"
integrity sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@ -4897,6 +5032,11 @@ mdn-data@2.0.30:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -5218,6 +5358,11 @@ optionator@^0.9.1:
type-check "^0.4.0"
word-wrap "^1.2.3"
orderedmap@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@ -5739,6 +5884,160 @@ proper-lockfile@^4.1.2:
retry "^0.12.0"
signal-exit "^3.0.2"
prosemirror-changeset@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz#eee3299cfabc7a027694e9abdc4e85505e9dd5e7"
integrity sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38"
integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.10.2"
prosemirror-dropcursor@^1.8.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz#2ed30c4796109ddeb1cf7282372b3850528b7228"
integrity sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz#e1144a83b79db7ed0ec32cd0e915a0364220af43"
integrity sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.5.0.tgz#ee21fc5de85a1473e3e3752015ffd6d649a06859"
integrity sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.4.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz#d2e935f6086e3801486b09222638f61dae89a570"
integrity sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472"
integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.13.1:
version "1.13.4"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz#4620e6a0580cd52b5fc8e352c7e04830cd4b3048"
integrity sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==
dependencies:
"@types/markdown-it" "^14.0.0"
markdown-it "^14.0.0"
prosemirror-model "^1.25.0"
prosemirror-menu@^1.2.4:
version "1.2.5"
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz#dea00e7b623cea89f4d76963bee22d2ac2343250"
integrity sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==
dependencies:
crelt "^1.0.0"
prosemirror-commands "^1.0.0"
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0, prosemirror-model@^1.25.4:
version "1.25.4"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.4.tgz#8ebfbe29ecbee9e5e2e4048c4fe8e363fcd56e7c"
integrity sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695"
integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==
dependencies:
prosemirror-model "^1.25.0"
prosemirror-schema-list@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5"
integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3, prosemirror-state@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.4.tgz#72b5e926f9e92dcee12b62a05fcc8a2de3bf5b39"
integrity sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.6.4:
version "1.8.5"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz#104427012e5a5da1d2a38c122efee8d66bdd5104"
integrity sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==
dependencies:
prosemirror-keymap "^1.2.3"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-transform "^1.10.5"
prosemirror-view "^1.41.4"
prosemirror-trailing-node@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
dependencies:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
version "1.11.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz#f5c5050354423dc83c6b083f6f1959ec86a3f9ba"
integrity sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.41.4:
version "1.41.5"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.5.tgz#3e152d14af633f2f5a73aba24e6130c63f643b2b"
integrity sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==
dependencies:
prosemirror-model "^1.20.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -5752,6 +6051,11 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
@ -6010,6 +6314,11 @@ rollbar@^2.26.4:
optionalDependencies:
decache "^3.0.5"
rope-sequence@^1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
run-applescript@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911"
@ -6792,6 +7101,11 @@ typed-function@^4.1.1:
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.1.tgz#38ce3cae31f4f513bcb263563fdad27b2afa73e8"
integrity sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@ -6934,7 +7248,7 @@ vue@^3.3.2:
"@vue/server-renderer" "3.3.4"
"@vue/shared" "3.3.4"
w3c-keyname@^2.2.4:
w3c-keyname@^2.2.0, w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==

Loading…
Cancel
Save