diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile index e0712a39a..64436b2bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ FROM base # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application diff --git a/Gemfile b/Gemfile index 1d8d39cd7..8c1219952 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,8 @@ gem "rqrcode" gem "redcarpet" gem "rouge" gem "jbuilder" +gem "actiontext-lexical", bc: "actiontext-lexical" +gem "image_processing", "~> 1.14" gem "platform_agent" # Telemetry and logging diff --git a/Gemfile.lock b/Gemfile.lock index 5b9940aad..0ed3a138e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/basecamp/actiontext-lexical + revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 + specs: + actiontext-lexical (0.1.0) + rails (>= 8.0.2) + GIT remote: https://github.com/basecamp/active_record-tenanted revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b @@ -188,6 +195,10 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -199,6 +210,9 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -236,6 +250,9 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -354,6 +371,9 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.32.0) @@ -448,6 +468,7 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES + actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -456,6 +477,7 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) + image_processing (~> 1.14) importmap-rails jbuilder kamal diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index aad4508fc..5b2a966af 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -177,6 +177,17 @@ --color-card-7: oklch(var(--lch-purple-medium)); --color-card-8: oklch(var(--lch-pink-medium)); + /* Colors: Syntax highlighting */ + --color-code-token__att: oklch(var(--lch-blue-dark)); + --color-code-token__comment: oklch(var(--lch-ink-medium)); + --color-code-token__function: oklch(var(--lch-purple-dark)); + --color-code-token__operator: oklch(var(--lch-red-dark)); + --color-code-token__property: oklch(var(--lch-purple-dark)); + --color-code-token__punctuation: oklch(var(--lch-ink-dark)); + --color-code-token__selector: oklch(var(--lch-green-dark)); + --color-code-token__variable: oklch(var(--lch-red-dark)); + + @media (prefers-color-scheme: dark) { --lch-canvas: 20% 0.0195 232.58; --lch-ink-inverted: var(--lch-black); diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css new file mode 100644 index 000000000..c0ab455a5 --- /dev/null +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -0,0 +1,113 @@ +lexical-editor { + .node--selected { + &:not(:has(img)) { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } + + &:has(img) { + img { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } + } + } +} + +.lexical-dialog-actions { + display: flex; + font-size: var(--text-x-small); + flex: 1 1 0%; + gap: var(--inline-space-half); + margin-block-start: var(--block-space-half); + + .btn { + --radius: 0.3em; + + inline-size: 100%; + justify-content: center; + } + + span { + inline-size: 100%; + } +} + +.lexical-editor__content { + --outline-size: max(2px, 0.08em); + + margin: var(--block-space-half) 0; + min-block-size: calc(7lh + var(--block-space)); + outline: 0; + padding: var(--block-space-half) var(--inline-space); + + * { + &:first-child { + margin-block-start: 0; + } + + &:last-child { + margin-block-end: 0; + } + } +} + +.lexical-editor--drag-over { + background-color: var(--color-selected); + border-radius: 4px; + outline: 2px dashed var(--color-selected-dark); +} + +lexical-toolbar { + background-color: inherit; + border-block-end: 1px solid var(--color-ink-light); + color: currentColor; + display: flex; + font-size: inherit; + margin: 0; + max-inline-size: 100%; + padding: 0.2em 0; + position: relative; + + dialog { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.5em; + box-shadow: var(--shadow); + color: var(--color-ink); + padding: var(--block-space) calc(var(--inline-space) * 1.5); + position: absolute; + } + + > button { + aspect-ratio: 4/3.5; + appearance: none; + background-color: transparent; + block-size: 2em; + border: none; + border-radius: 0.2em; + color: currentColor; + cursor: pointer; + display: grid; + font-size: inherit; + inline-size: auto; + place-items: center; + transition: background-color 300ms ease; + + svg { + -webkit-touch-callout: none; + block-size: 0.85em; + fill: currentColor; + grid-area: 1/1; + inline-size: auto; + user-select: none; + } + + &:is(:focus, :hover) { + background-color: var(--color-ink-lighter); + box-shadow: none; + } + + &:is(:active) { + background-color: var(--color-selected); + } + } +} diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 4e3c237f7..d13b60498 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -150,6 +150,13 @@ } } + .card-field__description { + lexical-toolbar { + border-block-start: 1px solid var(--color-ink-light); + margin-block-start: var(--block-margin) !important; + } + } + .card__content { flex: 2 1 auto; max-inline-size: 100%; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 2a5e47960..05d3518bc 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -1,7 +1,7 @@ @layer components { .comments { --avatar-size: 2.33em; - --comment-padding-block: var(--block-space); + --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; --reaction-size: 2rem; @@ -47,7 +47,7 @@ } .comment__avatar { - margin: calc(var(--comment-padding-block) * 0.5) calc(var(--comment-padding-inline) * -0.75); + margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75); z-index: 0; .comment--mine_ & { @@ -64,7 +64,7 @@ text-align: center; } - p { + * { &:first-child { margin-block-start: 0; } @@ -83,6 +83,8 @@ border-radius: 0.2em; padding: var(--comment-padding-block) + var(--comment-padding-inline) + calc(var(--comment-padding-block) * 1.5) var(--comment-padding-inline); word-wrap: break-word; } @@ -113,7 +115,7 @@ line-height: 1.2; max-inline-size: var(--comment-max); text-align: center; - + &::before { /* Make up space for lack of avatar */ @@ -133,17 +135,4 @@ text-align: center !important; } } - - .comment__input { - --input-border-size: 0; - --input-padding: 0; - - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - min-inline-size: 100%; - - @supports (field-sizing: content) { - field-sizing: content; - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - } - } } diff --git a/app/assets/stylesheets/markdown-content.css b/app/assets/stylesheets/markdown-content.css deleted file mode 100644 index 81589501b..000000000 --- a/app/assets/stylesheets/markdown-content.css +++ /dev/null @@ -1,110 +0,0 @@ -@layer components { - .markdown-content { - :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { - font-weight: 800; - hyphens: auto; - letter-spacing: -0.02ch; - line-height: 1.1; - margin-block: 0; - overflow-wrap: break-word; - text-wrap: balance; - } - - :where(h1, h2, h3, h4, h5, h6) { display: block; } - :where(h1, h2, h3, h4, h5, h6) { margin-block: 0.65em; } - - :is(h1, .h1) { font-size: 2em; } - :is(h2, .h2) { font-size: 1.5em; } - :is(h3, .h3) { font-size: 1.17em; } - :is(h4, .h4) { font-size: 1em; } - :is(h5, .h5) { font-size: 0.83em; } - :is(h6, .h6) { font-size: 0.67em; } - - :where(p, ul, ol, dl, blockquote, pre, figure, table, hr) { - margin-block: 0.65lh; - overflow-wrap: break-word; - text-wrap: pretty; - } - - :where(hr) { - border-color: var(--color-ink-medium); - border-style: var(--border-style, solid) none none; - margin: 2lh auto; - } - - :where(strong) { - font-weight: 700; - } - - :is(pre, code, .pre, .code) { - background-color: var(--color-ink-lightest); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.3em; - font-family: var(--font-mono); - font-size: 0.85em; - } - - :is(pre) { - overflow-x: auto; - /* text-wrap: nowrap; */ - } - - :where(code) { - padding: 0.1em 0.3em; - } - - :is(pre, .pre) { - border-radius: 0.5em; - padding: 0.5lh 2ch; - - :is(code, .code) { - background-color: transparent; - border: 0; - font-size: 1em; - padding: 0; - } - } - - :where(p) { - hyphens: auto; - letter-spacing: -0.005ch; - } - - :is(blockquote, .quote) { - font-style: italic; - margin: 0 3ch; - - :is(p) { - hyphens: none; - } - } - - :where(table, th, td) { - border: 1px solid var(--color-ink-medium); - } - - :where(table) { - border-collapse: collapse; - margin: 1lh 0; - } - - :where(th, td) { - padding: 0.2lh 1ch; - text-align: start; - } - - :where(th) { - border-block-end-width: 3px; - font-weight: 700; - } - - :where(img, video, embed, object) { - max-block-size: 32rem; - - /* Links should hug media contained within */ - a:has(&) { - display: inline-block; - } - } - } -} diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css new file mode 100644 index 000000000..52254f84d --- /dev/null +++ b/app/assets/stylesheets/rich-text-content.css @@ -0,0 +1,331 @@ +@layer components { + .rich-text-content { + --block-margin: 0.65lh; + + :where(h1, h2, h3, h4, h5, h6) { + display: block; + font-weight: 800; + hyphens: auto; + letter-spacing: -0.02ch; + line-height: 1.1; + margin-block: var(--block-margin); + overflow-wrap: break-word; + text-wrap: balance; + } + + :where(h1) { font-size: 2em; } + :where(h2) { font-size: 1.5em; } + :where(h3) { font-size: 1.17em; } + :where(h4) { font-size: 1em; } + :where(h5) { font-size: 0.83em; } + :where(h6) { font-size: 0.67em; } + + :where(p, ul, ol, dl, blockquote, figure, .attachment) { + margin-block: var(--block-margin); + overflow-wrap: break-word; + text-wrap: pretty; + } + + :where(b, strong) { + font-weight: 700; + } + + :where(p, blockquote) { + hyphens: auto; + letter-spacing: -0.005ch; + } + + :where(blockquote) { + border-inline-start: 0.25em solid var(--color-ink-lighter); + font-style: italic; + margin: var(--block-margin) 0; + padding: 0.5lh 2ch; + } + + :where(img, video, embed, object) { + margin-inline: auto; + max-block-size: 32rem; + + /* Links should hug media contained within */ + a:has(&) { + display: inline-block; + } + } + + /* Attachments */ + .attachment { + block-size: auto; + display: inline-block; + inline-size: 100%; + position: relative; + margin-inline: 0; + max-inline-size: 100%; + + &:is(figure) img { + block-size: auto; + display: block; + margin-inline: auto; + max-inline-size: 100%; + user-select: none; + } + + :where(progress) { + inline-size: 100%; + margin: auto; + } + } + + .attachment__caption { + color: var(--color-ink-dark); + display: flex; + font-size: var(--text-small); + flex-wrap: wrap; + line-height: 1.3; + column-gap: 0.5ch; + justify-content: center; + margin: var(--block-space-half) var(--inline-space); + text-align: center; + + .input { + inline-size: 100%; + max-inline-size: 100%; + text-align: center; + + @supports (field-sizing: content) { + field-sizing: content; + inline-size: auto; + min-inline-size: 20ch; + } + } + } + + .attachment__figure { + --hover-size: 0; + } + + .attachment--file { + align-items: center; + display: inline-flex; + + .attachment__caption { + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + text-align: start; + + strong { + color: var(--color-ink); + } + } + } + + .attachment__icon { + aspect-ratio: 4/5; + background-color: var(--color-canvas); + block-size: calc(2.5lh + 0.5ch); + border: 2px solid var(--color-ink-medium); + border-radius: 0.3em; + color: var(--color-ink-dark); + display: inline-grid; + font-size: var(--text-small); + font-weight: 800; + inline-size: auto; + overflow: clip; + padding-block-start: 0.4lh; + place-content: center; + position: relative; + text-transform: uppercase; + + &::before { + background-color: var(--color-ink-medium); + block-size: 0.5lh; + content: ""; + display: block; + inline-size: 100%; + inset: 0 0 auto; + position: absolute; + } + + .attachment--psd &, + .attachment--key &, + .attachment--sketch &, + .attachment--ai &, + .attachment--eps &, + .attachment--indd &, + .attachment--svg &, + .attachment--ppt &, + .attachment--pptx & { + color: oklch(var(--lch-red-dark)); + background-color: oklch(var(--lch-red-lightest)); + border-color: oklch(var(--lch-red-medium)); + + &::before { + background-color: oklch(var(--lch-red-medium)); + } + } + + .attachment--css &, + .attachment--php &, + .attachment--json &, + .attachment--htm &, + .attachment--html &, + .attachment--rb &, + .attachment--erb &, + .attachment--ts &, + .attachment--js & { + color: oklch(var(--lch-purple-dark)); + background-color: oklch(var(--lch-purple-lightest)); + border-color: oklch(var(--lch-purple-medium)); + + &::before { + background-color: oklch(var(--lch-purple-medium)); + } + } + + .attachment--txt &, + .attachment--pages &, + .attachment--rtf &, + .attachment--md &, + .attachment--doc &, + .attachment--docx & { + color: oklch(var(--lch-blue-dark)); + background-color: oklch(var(--lch-blue-lightest)); + border-color: oklch(var(--lch-blue-medium)); + + &::before { + background-color: oklch(var(--lch-blue-medium)); + } + } + + .attachment--csv &, + .attachment--numbers &, + .attachment--xls &, + .attachment--xlsx & { + color: oklch(var(--lch-green-dark)); + background-color: oklch(var(--lch-green-lightest)); + border-color: oklch(var(--lch-green-medium)); + + &::before { + background-color: oklch(var(--lch-green-medium)); + } + } + } + + .attachment__link { + --hover-size: 0; + + color: var(--color-link); + text-decoration: underline; + } + + /* Attachment junk gets wrapped in a paragraph causing unwanted space */ + p:has(action-text-attachment) { + display: none; + + + * { + margin-block-start: 0; + } + } + + /* Avoid extra space due to empty paragraphs */ + p:empty { + display: none; + } + + /* Code */ + :where(code, pre) { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.3em; + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 0.85em; + font-weight: 500; + margin-block: var(--block-margin); + padding: 0.1em 0.3em; + + &[data-language] { + border-radius: 0.5em; + display: block; + hyphens: none; + overflow-x: auto; + padding: 0.5lh 2ch; + tab-size: 2; + text-wrap: nowrap; + white-space: pre; + word-break: break-word; + + /* Keywords and attributes */ + .code-token__attr, + .token.atrule, + .token.attr, + .token.keyword { + color: var(--color-code-token__att); + } + + /* Constants, booleans, numbers, properties, tags */ + .code-token__property, + .token.boolean, + .token.constant, + .token.deleted, + .token.number, + .token.property, + .token.symbol, + .token.tag { + color: var(--color-code-token__property); + } + + /* Strings, selectors, and built-in constructs */ + .code-token__selector, + .token.builtin, + .token.char, + .token.inserted, + .token.selector, + .token.string { + color: var(--color-code-token__selector); + } + + /* Comments and meta information */ + .code-token__comment, + .token.cdata, + .token.comment, + .token.doctype, + .token.prolog { + color: var(--color-code-token__comment); + font-style: italic; + } + + /* Operators and symbolic entities */ + .code-token__operator, + .token.entity, + .token.operator, + .token.url { + color: var(--color-code-token__operator); + } + + /* Functions and class names */ + .code-token__function, + .token.class, + .token.class-name, + .token.function { + color: var(--color-code-token__function); + } + + /* Variables, regex, namespaces, important */ + .code-token__variable, + .token.important, + .token.namespace, + .token.regex, + .token.variable { + color: var(--color-code-token__variable); + } + + /* Punctuation */ + .code-token__punctuation, + .token.punctuation { + color: var(--color-code-token__punctuation); + } + } + } + } +} diff --git a/app/helpers/excerpt_helper.rb b/app/helpers/excerpt_helper.rb new file mode 100644 index 000000000..9ae0d3274 --- /dev/null +++ b/app/helpers/excerpt_helper.rb @@ -0,0 +1,12 @@ +module ExcerptHelper + def format_excerpt(content, length: 200) + return "" if content.blank? + + text = content.to_plain_text + text = text.gsub(/^>\s*(.*)$/m, '> \1') + text = text.gsub(/^[-*]\s*(.*)$/m, '• \1') + text = text.gsub(/^\d+\.\s*(.*)$/m) { |m| m } + text = text.gsub(/\s+/, " ").strip + text.truncate(length) + end +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 57fd63eff..8dab7c110 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -58,7 +58,7 @@ module NotificationsHelper def comment_notification_body(event) comment = event.eventable - "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" + "#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}" end def card_notification_title(card) diff --git a/app/javascript/application.js b/app/javascript/application.js index c2917aa3e..36d861cc7 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,4 +2,6 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" -import "house" + +import "actiontext-lexical" +import "@rails/actiontext" diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 5d9b89a72..5f9fc83a3 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -30,9 +30,10 @@ export default class extends Controller { async restoreContent() { await nextFrame() - const savedContent = localStorage.getItem(this.keyValue) + let savedContent = localStorage.getItem(this.keyValue) if (savedContent) { + savedContent = `
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
-<%= form.text_area :title, placeholder: "Name it…", class: "input input--textarea full-width borderless txt-align-start", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
-<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", @@ -8,10 +8,10 @@ data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %>
-~~$1~~")}(t),t=function(t){return t.replaceAll(/==(.*?)==/gm,"==$1==")}(t),t}(e),e=function(t){return t=function(t){return t=t.replaceAll(/^# (.*)$/gm,'# $1'),t=t.replaceAll(/^## (.*)$/gm,'## $1'),t=t.replaceAll(/^### (.*)$/gm,'### $1'),t=t.replaceAll(/^#### (.*)$/gm,'#### $1'),t=t.replaceAll(/^##### (.*)$/gm,'##### $1'),t=t.replaceAll(/^###### (.*)$/gm,'###### $1'),t}(t),t=function(t){return t.replaceAll(/^(\d+\.\s.*)$/gm,'$1')}(t),t=function(t){return t.replaceAll(/^(-\s.*)$/gm,'$1')}(t),t=function(t){return t.replaceAll(/^>(.*)$/gm,'>$1')}(t),t}(e),e=function(t){return function(t){return t.replaceAll(/\n---\n/gm,'\n---\n')}(t)}(e),e=function(t){return t.replaceAll(/(?[$1]($2)')}(e),e=function(t){return t.replaceAll(/!\[(.*?)\]\((.*?)\)/gm,'$&')}(e),e=function(t){return t=t.replaceAll(/^```(.*?)```$/gms,'```$1```'),t=t.replaceAll(/([^`])`([^`]+)`([^`])/gm,'$1`$2`$3'),t}(e),e=function(t){return t.replaceAll(/<!--(.*?)-->/gm,'<!--$1-->')}(e),e}class a{constructor(t,e){this.content=t,this.element=e,this.selection=new r(this),this.history=new o}select(t,e){this.selection.select({start:t,end:e})}insertText(t){this.replaceText(t,this.currentSelection.start,this.currentSelection.end)}replaceText(t,e,n){const s=this.content.slice(0,e),i=`${s}${t}${this.content.slice(n)}`,r={start:s.length+t.length,end:s.length+t.length};this.#h(),this.#d(i,r)}insertParagraph(t,e){this.select(t,e),this.selection.isBulletList?this.#u():this.selection.isNumberList?this.#m():this.replaceText("\n",t,e)}insertLink(t,e,n=""){const{start:s,end:i}=this.currentSelection,r=` ${n}[${t}](${e}) `;this.replaceText(r,s,i),this.selection.select({start:s+r.length,end:s+r.length})}insertFile(t,e,n){n.startsWith("image/")?this.insertImage(t,e):this.insertLink(t,e)}insertImage(t,e){this.insertLink(t,e,"!")}deleteText(t,e){this.replaceText("",Math.max(t,0),e)}deleteCurrentLine(){const{start:t,end:e}=this.currentLine;this.deleteText(t,e)}toggleBold(){this.#p(this.selection.isBold,"**")}toggleItalic(){this.#p(this.selection.isItalic,"_")}toggleStrikethrough(){this.#p(this.selection.isStrikethrough,"~~")}toggleCode(){const[t,e]=this.selection.isMultiline?["```\n","\n```"]:["`","`"];this.#p(this.selection.isCode,t,e)}toggleLink(){this.selection.isLink?this.#g():this.#v()}toggleQuote(){this.selection.isQuote?this.#f("> "):this.#b("> ")}toggleBulletList(){this.selection.isBulletList?this.#f("- "):this.#b("- ")}toggleNumberList(){this.selection.isNumberList?this.#f(/\d+\. /):this.#b(`${this.#L+1}. `)}undo(){const{content:e,start:n,end:s}=this.history.undo(this.#x);t(e)&&this.#d(e,{start:n,end:s})}redo(){const{content:e,start:n,end:s}=this.history.redo(this.#x);t(e)&&this.#d(e,{start:n,end:s})}render(){this.contentElement.innerHTML=l(this.content)+"