mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-17 03:46:21 +00:00
prettier + tachado
This commit is contained in:
parent
d00ab183ba
commit
d1cf673cc9
5 changed files with 246 additions and 159 deletions
|
@ -7,7 +7,6 @@
|
|||
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
|
||||
// layout file, like app/views/layouts/application.html.erb
|
||||
|
||||
|
||||
// Uncomment to copy all static images under ../images to the output folder and reference
|
||||
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
|
||||
// or the `imagePath` JavaScript helper below.
|
||||
|
@ -15,78 +14,96 @@
|
|||
// const images = require.context('../images', true)
|
||||
// const imagePath = (name) => images(name, true)
|
||||
|
||||
import tableDragger from "table-dragger"
|
||||
import tableDragger from 'table-dragger'
|
||||
|
||||
import {EditorState} from "prosemirror-state"
|
||||
import {EditorView} from "prosemirror-view"
|
||||
import {Schema, DOMParser} from "prosemirror-model"
|
||||
import {keymap} from "prosemirror-keymap"
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { Schema, DOMParser } from 'prosemirror-model'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
|
||||
// seleccionar cosas que no son texto
|
||||
import {gapCursor} from "prosemirror-gapcursor"
|
||||
import "prosemirror-gapcursor/style/gapcursor.css"
|
||||
import { gapCursor } from 'prosemirror-gapcursor'
|
||||
import 'prosemirror-gapcursor/style/gapcursor.css'
|
||||
|
||||
// convertir cosas como ">" a una quote: https://prosemirror.net/docs/ref/#inputrules
|
||||
import {inputRules, wrappingInputRule, textblockTypeInputRule,
|
||||
emDash, ellipsis} from "prosemirror-inputrules"
|
||||
import {
|
||||
inputRules,
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
emDash,
|
||||
ellipsis,
|
||||
} from 'prosemirror-inputrules'
|
||||
|
||||
import serializer from './serializer.js'
|
||||
import parser from './deserializer.js'
|
||||
import schema from './schema.js'
|
||||
|
||||
import {toggleMark, setBlockType, wrapIn, baseKeymap} from "prosemirror-commands"
|
||||
import {toggleWrap, toggleBlockType, toggleList,
|
||||
updateMark, removeMark} from "tiptap-commands"
|
||||
import {
|
||||
toggleMark,
|
||||
setBlockType,
|
||||
wrapIn,
|
||||
baseKeymap,
|
||||
} from 'prosemirror-commands'
|
||||
import {
|
||||
toggleWrap,
|
||||
toggleBlockType,
|
||||
toggleList,
|
||||
updateMark,
|
||||
removeMark,
|
||||
} from 'tiptap-commands'
|
||||
|
||||
import {startImageUpload, placeholderPlugin} from "./imageUpload"
|
||||
import { startImageUpload, placeholderPlugin } from './imageUpload'
|
||||
|
||||
const toggleBold = toggleMark(schema.marks.strong)
|
||||
const toggleItalic = toggleMark(schema.marks.em)
|
||||
const toggleHighlight = toggleMark(schema.marks.mark)
|
||||
const toggleStrikethrough = toggleMark(schema.marks.strikethrough)
|
||||
const wrapInQuote = toggleWrap(schema.nodes.blockquote)
|
||||
|
||||
const toggleUl = toggleList(schema.nodes.bullet_list, schema.nodes.list_item)
|
||||
const toggleOl = toggleList(schema.nodes.ordered_list, schema.nodes.list_item)
|
||||
|
||||
const generateHeadingToggle = level =>
|
||||
toggleBlockType(
|
||||
schema.nodes.heading,
|
||||
schema.nodes.paragraph,
|
||||
{ level: level },
|
||||
)
|
||||
toggleBlockType(schema.nodes.heading, schema.nodes.paragraph, {
|
||||
level: level,
|
||||
})
|
||||
const toggleH1 = generateHeadingToggle(1)
|
||||
const toggleH2 = generateHeadingToggle(2)
|
||||
|
||||
const makeLink = (state, dispatch) => {
|
||||
const {tr, selection, doc} = state
|
||||
const { tr, selection, doc } = state
|
||||
const type = schema.marks.link
|
||||
|
||||
const {from, to} = selection
|
||||
const { from, to } = selection
|
||||
const has = doc.rangeHasMark(from, to, type)
|
||||
|
||||
if (has) {
|
||||
tr.removeMark(from, to, type)
|
||||
} else {
|
||||
tr.addMark(from, to, type.create({
|
||||
href: window.prompt('elegir url', '//sutty.nl')
|
||||
}))
|
||||
tr.addMark(
|
||||
from,
|
||||
to,
|
||||
type.create({
|
||||
href: window.prompt('elegir url', '//sutty.nl'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return dispatch(tr)
|
||||
}
|
||||
|
||||
class SuttyEditor {
|
||||
constructor ({element, markdown, toolbar}) {
|
||||
constructor({ element, markdown, toolbar }) {
|
||||
this.state = this.buildState(markdown)
|
||||
this.view = this.buildView(element, this.state)
|
||||
this.hooks(toolbar)
|
||||
}
|
||||
|
||||
runCommand (command) {
|
||||
runCommand(command) {
|
||||
return command(this.view.state, this.view.dispatch, this.view)
|
||||
}
|
||||
|
||||
hooks (el) {
|
||||
hooks(el) {
|
||||
const setupBtn = (class_, action) => {
|
||||
const btnEl = el.querySelector('.' + class_ + '-btn')
|
||||
btnEl.type = 'button'
|
||||
|
@ -98,6 +115,7 @@ class SuttyEditor {
|
|||
setupBtn('bold', () => this.runCommand(toggleBold))
|
||||
setupBtn('italic', () => this.runCommand(toggleItalic))
|
||||
setupBtn('highlight', () => this.runCommand(toggleHighlight))
|
||||
setupBtn('strikethrough', () => this.runCommand(toggleStrikethrough))
|
||||
setupBtn('quote', () => this.runCommand(wrapInQuote))
|
||||
setupBtn('link', () => this.runCommand(makeLink))
|
||||
setupBtn('ul', () => this.runCommand(toggleUl))
|
||||
|
@ -108,8 +126,8 @@ class SuttyEditor {
|
|||
const imgInput = el.querySelector('.img-input')
|
||||
imgInput.addEventListener('change', event => {
|
||||
if (
|
||||
this.view.state.selection.$from.parent.inlineContent
|
||||
&& event.target.files.length
|
||||
this.view.state.selection.$from.parent.inlineContent &&
|
||||
event.target.files.length
|
||||
) {
|
||||
const file = event.target.files[0]
|
||||
event.target.value = ''
|
||||
|
@ -119,12 +137,14 @@ class SuttyEditor {
|
|||
})
|
||||
}
|
||||
|
||||
buildState (markdown) {
|
||||
buildState(markdown) {
|
||||
return EditorState.create({
|
||||
schema,
|
||||
...(markdown ? {
|
||||
doc: parser.parse(markdown)
|
||||
} : {}),
|
||||
...(markdown
|
||||
? {
|
||||
doc: parser.parse(markdown),
|
||||
}
|
||||
: {}),
|
||||
plugins: [
|
||||
gapCursor(),
|
||||
keymap(baseKeymap),
|
||||
|
@ -133,18 +153,17 @@ class SuttyEditor {
|
|||
rules: [
|
||||
//emDash, // -- => —
|
||||
//ellipsis, // ... => …
|
||||
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
buildView (element, state) {
|
||||
return new EditorView(element, {state})
|
||||
buildView(element, state) {
|
||||
return new EditorView(element, { state })
|
||||
}
|
||||
|
||||
getMarkdown () {
|
||||
getMarkdown() {
|
||||
return serializer.serialize(this.view.state.doc)
|
||||
}
|
||||
}
|
||||
|
@ -155,35 +174,39 @@ document.addEventListener('turbolinks:load', e => {
|
|||
for (const el of editorEls) {
|
||||
// conseguir textarea con el markdown
|
||||
const textareaEl = document.querySelector(
|
||||
`.sutty-editor-content[data-sutty-identifier="${el.dataset.suttyIdentifier}"]`
|
||||
`.sutty-editor-content[data-sutty-identifier="${el.dataset.suttyIdentifier}"]`,
|
||||
)
|
||||
|
||||
// conseguir toolbar
|
||||
const toolbarEl = document.querySelector(
|
||||
`.sutty-editor-toolbar[data-sutty-identifier="${el.dataset.suttyIdentifier}"]`
|
||||
`.sutty-editor-toolbar[data-sutty-identifier="${el.dataset.suttyIdentifier}"]`,
|
||||
)
|
||||
|
||||
// ocultarlo (no se oculta originalmente para usuaries noscript)
|
||||
textareaEl.style.display = 'none'
|
||||
|
||||
|
||||
// crear editor con el markdown del textarea
|
||||
const editor = new SuttyEditor({
|
||||
element: el,
|
||||
markdown: textareaEl.value,
|
||||
toolbar: toolbarEl,
|
||||
toolbar: toolbarEl,
|
||||
})
|
||||
|
||||
// hookear a la form para inyectar el contenido del editor al textarea oculto para que se mande
|
||||
textareaEl.form.addEventListener('submit', e => {
|
||||
console.log('enviando formulario: inyectando markdown en textarea...')
|
||||
try {
|
||||
textareaEl.value = editor.getMarkdown()
|
||||
} catch (error) {
|
||||
console.error('no se pudo inyectar markdown!', error)
|
||||
// TODO: mostrar esto bien
|
||||
e.preventDefault()
|
||||
}
|
||||
}, true)
|
||||
textareaEl.form.addEventListener(
|
||||
'submit',
|
||||
e => {
|
||||
console.log('enviando formulario: inyectando markdown en textarea...')
|
||||
try {
|
||||
textareaEl.value = editor.getMarkdown()
|
||||
} catch (error) {
|
||||
console.error('no se pudo inyectar markdown!', error)
|
||||
// TODO: mostrar esto bien
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('algo falló en la carga del editor', error)
|
||||
|
@ -199,10 +222,11 @@ document.addEventListener('turbolinks:load', e => {
|
|||
dragHandler: '.handle',
|
||||
}).on('drop', (from, to, el, mode) => {
|
||||
Array.from(document.querySelectorAll('.reorder'))
|
||||
.reverse()
|
||||
.map((o,i) => o.value = i)
|
||||
.reverse()
|
||||
.map((o, i) => (o.value = i))
|
||||
|
||||
Array.from(document.querySelectorAll('.submit-reorder'))
|
||||
.map(s => s.classList.remove('d-none'))
|
||||
Array.from(document.querySelectorAll('.submit-reorder')).map(s =>
|
||||
s.classList.remove('d-none'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,37 +1,55 @@
|
|||
// From https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
|
||||
import {MarkdownParser} from 'prosemirror-markdown'
|
||||
import markdownit from "markdown-it"
|
||||
import markdownitMark from "markdown-it-mark"
|
||||
import { MarkdownParser } from 'prosemirror-markdown'
|
||||
import markdownit from 'markdown-it'
|
||||
import markdownitMark from 'markdown-it-mark'
|
||||
|
||||
import schema from './schema.js'
|
||||
|
||||
export default new MarkdownParser(
|
||||
schema,
|
||||
markdownit("commonmark", {html: false}).use(markdownitMark),
|
||||
markdownit('commonmark', { html: false })
|
||||
.use(markdownitMark)
|
||||
.enable('strikethrough'),
|
||||
{
|
||||
blockquote: {block: "blockquote"},
|
||||
paragraph: {block: "paragraph"},
|
||||
list_item: {block: "list_item"},
|
||||
bullet_list: {block: "bullet_list"},
|
||||
ordered_list: {block: "ordered_list", getAttrs: tok => ({order: +tok.attrGet("start") || 1})},
|
||||
heading: {block: "heading", getAttrs: tok => ({level: +tok.tag.slice(1)})},
|
||||
code_block: {block: "code_block"},
|
||||
fence: {block: "code_block", getAttrs: tok => ({params: tok.info || ""})},
|
||||
hr: {node: "horizontal_rule"},
|
||||
image: {node: "image", getAttrs: tok => ({
|
||||
src: tok.attrGet("src"),
|
||||
title: tok.attrGet("title") || null,
|
||||
alt: tok.children[0] && tok.children[0].content || null,
|
||||
})},
|
||||
hardbreak: {node: "hard_break"},
|
||||
blockquote: { block: 'blockquote' },
|
||||
paragraph: { block: 'paragraph' },
|
||||
list_item: { block: 'list_item' },
|
||||
bullet_list: { block: 'bullet_list' },
|
||||
ordered_list: {
|
||||
block: 'ordered_list',
|
||||
getAttrs: tok => ({ order: +tok.attrGet('start') || 1 }),
|
||||
},
|
||||
heading: {
|
||||
block: 'heading',
|
||||
getAttrs: tok => ({ level: +tok.tag.slice(1) }),
|
||||
},
|
||||
code_block: { block: 'code_block' },
|
||||
fence: {
|
||||
block: 'code_block',
|
||||
getAttrs: tok => ({ params: tok.info || '' }),
|
||||
},
|
||||
hr: { node: 'horizontal_rule' },
|
||||
image: {
|
||||
node: 'image',
|
||||
getAttrs: tok => ({
|
||||
src: tok.attrGet('src'),
|
||||
title: tok.attrGet('title') || null,
|
||||
alt: (tok.children[0] && tok.children[0].content) || null,
|
||||
}),
|
||||
},
|
||||
hardbreak: { node: 'hard_break' },
|
||||
|
||||
em: {mark: "em"},
|
||||
strong: {mark: "strong"},
|
||||
mark: {mark: "mark"},
|
||||
link: {mark: "link", getAttrs: tok => ({
|
||||
href: tok.attrGet("href"),
|
||||
title: tok.attrGet("title") || null
|
||||
})},
|
||||
code_inline: {mark: "code"},
|
||||
em: { mark: 'em' },
|
||||
strong: { mark: 'strong' },
|
||||
mark: { mark: 'mark' },
|
||||
s: { mark: 'strikethrough' },
|
||||
link: {
|
||||
mark: 'link',
|
||||
getAttrs: tok => ({
|
||||
href: tok.attrGet('href'),
|
||||
title: tok.attrGet('title') || null,
|
||||
}),
|
||||
},
|
||||
code_inline: { mark: 'code' },
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import {schema as defaultMarkdownSchema, defaultMarkdownParser} from "prosemirror-markdown"
|
||||
import {Schema} from "prosemirror-model"
|
||||
import { schema as defaultMarkdownSchema } from 'prosemirror-markdown'
|
||||
import { Schema } from 'prosemirror-model'
|
||||
|
||||
export default new Schema({
|
||||
nodes: defaultMarkdownSchema.spec.nodes,
|
||||
marks: defaultMarkdownSchema.spec.marks.addBefore('text', 'mark', {
|
||||
toDOM: node => ['mark'],
|
||||
parseDOM: [{ tag: 'mark' }],
|
||||
}),
|
||||
marks: defaultMarkdownSchema.spec.marks
|
||||
.addBefore('text', 'mark', {
|
||||
toDOM: node => ['mark'],
|
||||
parseDOM: [{ tag: 'mark' }],
|
||||
})
|
||||
.addBefore('text', 'strikethrough', {
|
||||
toDOM: node => ['s'],
|
||||
parseDOM: [{ tag: 's' }],
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
|
@ -1,74 +1,114 @@
|
|||
// From https://raw.githubusercontent.com/ProseMirror/prosemirror-markdown/master/src/to_markdown.js
|
||||
import {MarkdownSerializer} from "prosemirror-markdown"
|
||||
import { MarkdownSerializer } from 'prosemirror-markdown'
|
||||
|
||||
export default new MarkdownSerializer({
|
||||
blockquote(state, node) {
|
||||
state.wrapBlock("> ", null, node, () => state.renderContent(node))
|
||||
},
|
||||
code_block(state, node) {
|
||||
state.write("```" + (node.attrs.params || "") + "\n")
|
||||
state.text(node.textContent, false)
|
||||
state.ensureNewLine()
|
||||
state.write("```")
|
||||
state.closeBlock(node)
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(state.repeat("#", node.attrs.level) + " ")
|
||||
state.renderInline(node)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
horizontal_rule(state, node) {
|
||||
state.write(node.attrs.markup || "---")
|
||||
state.closeBlock(node)
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ")
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
let start = node.attrs.order || 1
|
||||
let maxW = String(start + node.childCount - 1).length
|
||||
let space = state.repeat(" ", maxW + 2)
|
||||
state.renderList(node, space, i => {
|
||||
let nStr = String(start + i)
|
||||
return state.repeat(" ", maxW - nStr.length) + nStr + ". "
|
||||
})
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node)
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
state.write("![" + state.esc(node.attrs.alt || "") + "](" + state.esc(node.attrs.src) +
|
||||
(node.attrs.title ? " " + state.quote(node.attrs.title) : "") + ")")
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i++)
|
||||
if (parent.child(i).type != node.type) {
|
||||
state.write("\\\n")
|
||||
return
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text)
|
||||
}
|
||||
}, {
|
||||
em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true},
|
||||
strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true},
|
||||
mark: {open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "["
|
||||
export default new MarkdownSerializer(
|
||||
{
|
||||
blockquote(state, node) {
|
||||
state.wrapBlock('> ', null, node, () => state.renderContent(node))
|
||||
},
|
||||
code_block(state, node) {
|
||||
state.write('```' + (node.attrs.params || '') + '\n')
|
||||
state.text(node.textContent, false)
|
||||
state.ensureNewLine()
|
||||
state.write('```')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(state.repeat('#', node.attrs.level) + ' ')
|
||||
state.renderInline(node)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
horizontal_rule(state, node) {
|
||||
state.write(node.attrs.markup || '---')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ')
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
let start = node.attrs.order || 1
|
||||
let maxW = String(start + node.childCount - 1).length
|
||||
let space = state.repeat(' ', maxW + 2)
|
||||
state.renderList(node, space, i => {
|
||||
let nStr = String(start + i)
|
||||
return state.repeat(' ', maxW - nStr.length) + nStr + '. '
|
||||
})
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node)
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
state.write(
|
||||
'![' +
|
||||
state.esc(node.attrs.alt || '') +
|
||||
'](' +
|
||||
state.esc(node.attrs.src) +
|
||||
(node.attrs.title ? ' ' + state.quote(node.attrs.title) : '') +
|
||||
')',
|
||||
)
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i++)
|
||||
if (parent.child(i).type != node.type) {
|
||||
state.write('\\\n')
|
||||
return
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text)
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1) ? ">"
|
||||
: "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")"
|
||||
}
|
||||
},
|
||||
code: {open(_state, _mark, parent, index) { return backticksFor(parent.child(index), -1) },
|
||||
close(_state, _mark, parent, index) { return backticksFor(parent.child(index - 1), 1) },
|
||||
escape: false}
|
||||
})
|
||||
{
|
||||
em: {
|
||||
open: '*',
|
||||
close: '*',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strong: {
|
||||
open: '**',
|
||||
close: '**',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
mark: {
|
||||
open: '==',
|
||||
close: '==',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strikethrough: {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? '<' : '['
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? '>'
|
||||
: '](' +
|
||||
state.esc(mark.attrs.href) +
|
||||
(mark.attrs.title ? ' ' + state.quote(mark.attrs.title) : '') +
|
||||
')'
|
||||
},
|
||||
},
|
||||
code: {
|
||||
open(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index), -1)
|
||||
},
|
||||
close(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index - 1), 1)
|
||||
},
|
||||
escape: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
%button.bold-btn= "bold"
|
||||
%button.italic-btn= "italic"
|
||||
%button.highlight-btn= "highlight"
|
||||
%button.strikethrough-btn= "strikethrough"
|
||||
%button.quote-btn= "quote"
|
||||
%button.ul-btn= "ul"
|
||||
%button.ol-btn= "ol"
|
||||
|
|
Loading…
Reference in a new issue