Merge pull request #17 from Brecert/simple-markdown

Simple markdown
This commit is contained in:
Supertiger 2019-11-13 09:24:53 +00:00 committed by GitHub
commit e15bd97532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 318 additions and 294 deletions

View file

@ -12,11 +12,9 @@
"filesize": "^4.1.2",
"highlight.js": "^9.15.8",
"jquery": "^3.4.0",
"markdown-it": "^9.0.1",
"markdown-it-chat-formatter": "^0.1.1",
"match-sorter": "^2.3.0",
"particles.js": "^2.0.0",
"simple-markdown": "^0.6.1",
"simple-markdown": "^0.7.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"twemoji": "^11.3.0",

View file

@ -21,8 +21,7 @@
<div class="username" @click="openUserInformation">{{ this.$props.username }}</div>
<div class="date">{{ getDate }}</div>
</div>
<div class="content-message" v-html="formatMessage" />
<SimpleMarkdown class="content-message" :message="message" />
<div class="file-content" v-if="getFile">
<div class="icon">
<i class="material-icons">insert_drive_file</i>
@ -87,6 +86,7 @@
<script>
import ProfilePicture from "@/components/ProfilePictureTemplate.vue";
import SimpleMarkdown from "./SimpleMarkdown.vue";
import messageEmbedTemplate from "./messageEmbedTemplate";
import messageFormatter from "@/utils/messageFormatter.js";
import config from "@/config.js";
@ -115,7 +115,8 @@ export default {
],
components: {
ProfilePicture,
messageEmbedTemplate
messageEmbedTemplate,
SimpleMarkdown
},
data() {
return {
@ -504,36 +505,4 @@ export default {
@media (max-width: 468px) {
}
</style>
<style>
.code-inline {
background: rgba(0, 0, 0, 0.322);
}
.msg-link {
color: rgb(86, 159, 253);
}
pre {
padding: 0;
margin: 0;
}
.codeblock {
background-color: rgba(0, 0, 0, 0.397);
padding: 5px;
border-radius: 5px;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.content-message img.emoji {
object-fit: contain;
height: 2em;
width: 2em;
margin: 1px;
vertical-align: -9px;
}
</style>
</style>

View file

@ -0,0 +1,55 @@
<template>
<div class="formatted-content" v-html="markdown">
</div>
</template>
<script>
import Vue from 'vue'
import messageFormatter from '@/utils/messageFormatter'
export default {
props: {
message: String,
require: true
},
computed: {
markdown: function() {
return messageFormatter(this.message)
}
}
}
</script>
<style>
pre {
padding: 0;
margin: 0;
}
.codeblock {
background-color: rgba(0, 0, 0, 0.397);
padding: 5px;
border-radius: 5px;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.content-message img.emoji {
object-fit: contain;
height: 2em;
width: 2em;
margin: 1px;
vertical-align: -9px;
}
.inline-code {
background: rgba(0, 0, 0, 0.322);
}
.link {
color: rgb(86, 159, 253);
}
</style>

View file

@ -1,117 +0,0 @@
import config from "@/config.js";
/* 58: ':' */
/* 60: '<' */
/* 62: '>' */
/* example: '<:cat_1:1hvDmIdozFp2vTTkEIsO-wBHPaYRkGmlP>' */
function skipUntil(state, pos, code) {
for (let max = state.src.length; pos < max; pos++) {
if (state.src.charCodeAt(pos) === code) { break; }
}
return pos;
}
// old
function parseUntil(state, fromPos, until, maxLength = 16) {
let max = state.posMax
let found = false
let pos = state.pos + fromPos
let oldPos = state.pos
let end = -1
while(pos < max && pos - oldPos < maxLength) {
let marker = state.src.charCodeAt(pos)
if(marker === until) {
found = true;
break;
}
// state.md.inline.skipToken(state);
pos += 1
}
if(found) {
end = pos
}
return end
}
function render_custom_emoji(tokens, idx) {
return ''
}
function parseEmojiName(state, nameStart) {
// return parseUntil(state, nameStart, 58)
return skipUntil(state, nameStart, 58)
}
function replace_custom_emoji(state, silent) {
let pos = state.pos
let max = state.posMax
// if begins with <
if (state.src.charCodeAt(pos) !== 60) { return false; }
pos += 1
// if the next character is not ':' then it's not a custom emoji
if(state.src.charCodeAt(pos) !== 58) { return false; }
// parse the emoji name
let nameStart = pos + 1
let nameEnd = parseEmojiName(state, nameStart)
// console.log(nameEnd, parseUntil(state,nameStart,58))
// parser failed to find another ':', so it's not a valid emoji
if((nameEnd+1) > max || nameEnd < 0 || nameEnd - nameStart <= 0) { return false; }
let emojiName = state.src.slice(nameStart, nameEnd)
pos = nameEnd + 1
// parse until '>'
let idStart = pos
let idEnd = skipUntil(state, pos, 62);
if((idEnd+1) > max || idEnd < 0 || idEnd - idStart <= 1) { return false; }
// console.log(idStart, idEnd)
let emojiID = state.src.slice(idStart, idEnd)
if(!silent) {
state.pos = idStart
state.posMax = idEnd
let token = state.push('custom_emoji', 'img', 0);
token.attrs = [[ 'src', (`${config.domain}/media/${emojiID}`) ], [ 'alt', (emojiName) ]]
}
state.pos = idEnd + 1
state.posMax = max
return true
}
const emojiRe = /<:[\w\d]+:[\w\d]+>/
export default function custom_emoji_plugin(md, opts) {
md.renderer.rules.custom_emoji = (tokens, idx) => {
let token = tokens[idx]
const srcIdx = token.attrIndex('src');
const altIdx = token.attrIndex('alt');
let src = encodeURI(md.utils.escapeHtml(token.attrs[srcIdx][1]))
let alt = md.utils.escapeHtml(token.attrs[altIdx][1])
return `<${md.utils.escapeHtml(token.tag)} class="emoji" title=${alt} alt=${alt} src=${src} />`
}
md.inline.ruler.push('custom_emoji', replace_custom_emoji)
}

View file

@ -1,10 +0,0 @@
export default function formatCode(md, opts) {
const defaultRender = md.renderer.rules.code_inline || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.code_inline = function (tokens, idx, options, env, self) {
tokens[idx].attrPush(['class', 'code-inline']);
return defaultRender(tokens, idx, options, env, self);
};
}

View file

@ -1,12 +0,0 @@
export default function formatLink(md, opts) {
const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
tokens[idx].attrPush(['target', '_blank']); // add new attribute
tokens[idx].attrPush(['class', 'msg-link']);
// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self);
};
}

View file

@ -1,83 +0,0 @@
function fence(state, startLine, endLine, silent) {
var marker, len, params, nextLine, mem, token, markup,
haveEndMarker = false,
pos = state.bMarks[startLine] + state.tShift[startLine],
max = state.eMarks[startLine];
// if it's indented more than 3 spaces, it should be a code block
// if (state.sCount[startLine] - state.blkIndent >= 4) { return false; }
if (pos + 3 > max) { return false; }
marker = state.src.charCodeAt(pos);
if (marker !== 0x60 /* ` */) {
return false;
}
// scan marker length
let count = state.skipChars(pos, marker);
len = count - pos;
pos += 3
if (len < 3) { return false; }
markup = state.src.slice(mem, pos);
params = state.src.slice(pos, max);
if (marker === 0x60 /* ` */) {
if (params.indexOf(String.fromCharCode(marker)) >= 0) {
return false;
}
}
// Since start is found, we can report success here in validation mode
if (silent) { return true; }
// search end of block
nextLine = startLine;
for (;;) {
nextLine++;
if (nextLine >= endLine) {
return false
}
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine];
max = state.eMarks[nextLine];
if (state.src.charCodeAt(pos) !== marker) { continue; }
pos = state.skipChars(pos, marker);
// closing code fence must be at least as long as the opening one
if (pos - mem < 3) { continue; }
// make sure tail has spaces only
pos = state.skipSpaces(pos);
if (pos < max) { continue; }
haveEndMarker = true;
// found!
break;
}
// If a fence has heading spaces, they should be removed from its inner block
len = state.sCount[startLine];
state.line = nextLine + (haveEndMarker ? 1 : 0);
token = state.push('fence', 'code', 0);
token.info = params;
token.content = state.getLines(startLine + 1, nextLine, len, true);
token.markup = markup;
token.map = [ startLine, state.line ];
return true;
}
export default function normalizeFence(md, opts) {
md.block.ruler.at('fence', fence, {alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]})
}

View file

@ -0,0 +1,36 @@
import * as SimpleMarkdown from 'simple-markdown'
import hljs from 'highlight.js'
export default (order) => { return {
order: order++,
match: function(source) {
return /^ *```(?:(\S+) *)?\n?((?:[^`])+)```/.exec(source)
},
parse: function(capture, parse, state) {
return {
lang: capture[1] || undefined,
content: capture[2]
}
},
html: function(node, output) {
const className = node.lang ? `language-${node.lang}` : undefined
let content = SimpleMarkdown.sanitizeText(node.content)
if(node.lang) {
content = hljs.highlight(node.lang, node.content, true).value
}
const codeblock = SimpleMarkdown.htmlTag("div", content, {
class: "codeblock"
})
const code = SimpleMarkdown.htmlTag("code", codeblock, {
class: className
})
return SimpleMarkdown.htmlTag("pre", code)
}
}}

View file

@ -0,0 +1,24 @@
import * as SimpleMarkdown from 'simple-markdown'
import config from "@/config.js";
export default (order) => { return {
order: order++,
match: function(source) {
return /^<:([\w\d_]+):([\w\d_]+)>/.exec(source)
},
parse: function(capture, parse, state) {
return {
name: capture[1],
id: capture[2]
}
},
html: function(node, output) {
return SimpleMarkdown.htmlTag('img', '', {
class: "emoji custom-emoji",
title: node.name,
src: `${config.domain}/media/${node.id}`,
alt: node.name
})
}
}}

View file

@ -0,0 +1,17 @@
import * as SimpleMarkdown from 'simple-markdown'
export default (order) => { return {
order: order++,
match: function(source) {
return /^```([^\n]+)```(?!`)/.exec(source)
},
parse: function(capture, parse, state) {
return {
type: 'codeblock',
lang: undefined,
content: capture[1]
}
},
html: null
}}

View file

@ -0,0 +1,41 @@
import * as SimpleMarkdown from 'simple-markdown'
import LinkifyIt from 'linkify-it'
const linkify = LinkifyIt()
export default (order) => { return {
order: order++,
match: function(source) {
const match = linkify.match(source)
if(match === null) {
return null
}
const converted = [
match[0].raw,
match[0].text,
match[0].url,
]
return converted
},
parse: function(capture, parse, state) {
return {
content: {
type: 'text',
content: capture[1]
},
url: capture[2]
}
},
html: function(node, output) {
return SimpleMarkdown.htmlTag("a", output(node.content), {
href: node.url,
class: "link",
target: "_blank"
})
}
}}

View file

@ -0,0 +1,20 @@
import * as SimpleMarkdown from 'simple-markdown'
export default (order) => { return {
order: order++,
match: function(source) {
return /^~~([\s\S]+?)~~(?!~)/.exec(source)
},
parse: function(capture, parse, state) {
return {
content: parse(capture[1], state)
}
},
html: function(node, output) {
return SimpleMarkdown.htmlTag("s", output(node.content), {
class: "strikeout"
})
}
}}

View file

@ -0,0 +1,20 @@
import * as SimpleMarkdown from 'simple-markdown'
export default (order) => { return {
order: order++,
match: function(source) {
return /^__([\s\S]+?)__(?!_)/.exec(source)
},
parse: function(capture, parse, state) {
return {
content: parse(capture[1], state)
}
},
html: function(node, output) {
return SimpleMarkdown.htmlTag("u", output(node.content), {
class: "underline"
})
}
}}

View file

@ -2,46 +2,64 @@ import twemoji from "twemoji";
import emojiParser from "@/utils/emojiParser";
import config from "@/config.js";
import customEmoji from "./markdown-it-plugins/customEmoji";
import formatLink from "./markdown-it-plugins/formatLink";
import formatCode from "./markdown-it-plugins/formatCode";
import normalizeFence from "./markdown-it-plugins/normalizeFence";
import * as SimpleMarkdown from 'simple-markdown'
import { htmlTag as h } from 'simple-markdown'
import hljs from "highlight.js";
import MarkdownIt from "markdown-it";
import chatPlugin from "markdown-it-chat-formatter/dist-src/plugin";
import codeblock from './markdown-rules/codeblock'
import underline from './markdown-rules/underline'
import strikeout from './markdown-rules/strikeout'
import inlineCodeblock from './markdown-rules/inlineCodeblock'
import link from './markdown-rules/link'
import customEmoji from './markdown-rules/customEmoji'
const markdown = new MarkdownIt({
highlight: function(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<div class="codeblock"><code>' +
hljs.highlight(lang, str, true).value +
"</code></div>"
);
} catch (err) {
console.error(err);
}
}
let order = 0; // order the below rules as declared below rather than by the original defaultRules order:
return (
'<div class="codeblock"><code>' +
markdown.utils.escapeHtml(str) +
"</code></div>"
);
const rules = {
inlineCodeblock: inlineCodeblock(order++),
codeblock: codeblock(order++),
underline: underline(order++),
strikeout: strikeout(order++),
link: link(order++),
customEmoji: customEmoji(order++),
strong: Object.assign({}, SimpleMarkdown.defaultRules.strong, {
order: order++,
}),
em: Object.assign({}, SimpleMarkdown.defaultRules.em, {
order: order++,
}),
inlineCode: Object.assign({}, SimpleMarkdown.defaultRules.inlineCode, {
order: order++,
html: function(node, parse, state) {
return SimpleMarkdown.htmlTag("code", SimpleMarkdown.sanitizeText(node.content), {
class: "inline-code"
})
}
}),
text: Object.assign({}, SimpleMarkdown.defaultRules.text, {
order: order++,
}),
};
const parse = SimpleMarkdown.parserFor(rules);
const output = SimpleMarkdown.outputFor(rules, 'html');
const markdownToHtml = function(source, state) {
// if you don't have a paragraph rule, you probably want to default `state.inline` to true, to
// indicate to the bold rule that it is parsing inline text:
if (rules.paragraph == null) {
state.inline = true;
}
})
.use(normalizeFence)
.use(chatPlugin)
// .use(customEmoji)
.use(formatLink)
.use(formatCode);
const parsedContentTree = parse(source, state);
return output(parsedContentTree, state);
};
export default message => {
message = markdown.render(message).trim();
message = markdownToHtml(message || '', { inline: false })
message = emojiParser.replaceEmojis(message);
return message;

View file

@ -817,16 +817,34 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.8.tgz#551466be11b2adc3f3d47156758f610bd9f6b1d8"
integrity sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==
"@types/node@>=10.0.0":
version "12.12.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.7.tgz#01e4ea724d9e3bd50d90c11fd5980ba317d8fa11"
integrity sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/q@^1.5.1":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/react@>=16.0.0":
version "16.9.11"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120"
integrity sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/socket.io-client@1.4.32":
version "1.4.32"
resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14"
@ -2736,6 +2754,11 @@ csso@^3.5.1:
dependencies:
css-tree "1.0.0-alpha.29"
csstype@^2.2.0:
version "2.6.7"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
current-script-polyfill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz#f31cf7e4f3e218b0726e738ca92a02d3488ef615"
@ -7418,6 +7441,14 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
simple-markdown@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/simple-markdown/-/simple-markdown-0.7.1.tgz#48f2f65ad9f7faa5922347e9ab6baf66ab3195bc"
integrity sha512-9p92kPFjaDER1yuDgVW1UzFVQoL46HBonniqOLKnCmqn0vlxKkHIxRODBJdHLplo44tOtmnGxPTbUZ1HpYkHyg==
dependencies:
"@types/node" ">=10.0.0"
"@types/react" ">=16.0.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@ -7545,6 +7576,11 @@ sort-keys@^2.0.0:
dependencies:
is-plain-obj "^1.0.0"
sortablejs@^1.10.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.1.tgz#3d52b00f871be00f00f84d99a60d120bf3dfe52c"
integrity sha512-N6r7GrVmO8RW1rn0cTdvK3JR0BcqecAJ0PmYMCL3ZuqTH3pY+9QyqkmJSkkLyyDvd+AJnwaxTP22Ybr/83V9hQ==
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@ -8380,6 +8416,11 @@ uws@^10.148.1:
resolved "https://registry.yarnpkg.com/uws/-/uws-10.148.2.tgz#f01652a0b4bb941cb18bb7a6248d780fd0150245"
integrity sha1-8BZSoLS7lByxi7emJI14D9AVAkU=
v-clipboard@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/v-clipboard/-/v-clipboard-2.2.2.tgz#6144e3c5b895ead35463386aa660360a9f88fabf"
integrity sha512-8Nch/q4j4e5BqHFuKUReKBvB7lzn9FQTEuPa54pmfX44VYhWnxAoSHuMwm2Qf9EnyCSEmczqj2VYPsU2BEe6Mw==
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -8539,6 +8580,13 @@ vue@^2.5.17:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
vuedraggable@^2.23.2:
version "2.23.2"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.2.tgz#0d95d7fdf4f02f56755a26b3c9dca5c7ca9cfa72"
integrity sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ==
dependencies:
sortablejs "^1.10.1"
vuex@^3.0.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.1.tgz#0c264bfe30cdbccf96ab9db3177d211828a5910e"