mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-04 23:02:18 +02:00
md parsing: cleanup and refactoring
This commit is contained in:
parent
3bcebdfe3a
commit
1a32d7a77e
|
@ -1,213 +1,16 @@
|
|||
<script lang="ts">
|
||||
import {defineComponent, PropType, h, VNode, RendererNode, RendererElement} from "vue";
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import parse from "../js/helpers/parse";
|
||||
import {
|
||||
ParseFragment,
|
||||
createPlaceholder,
|
||||
rehydrate,
|
||||
parseMd,
|
||||
idGenerator,
|
||||
} from "../js/helpers/mdHelpers";
|
||||
import type {ClientMessage, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
const MD_PLACEHOLDER_TAG = "thelounge-mdparse-placeholder";
|
||||
const MD_PLACEHOLDER_OPEN = `<${MD_PLACEHOLDER_TAG}>`;
|
||||
const MD_PLACEHOLDER_CLOSE = `</${MD_PLACEHOLDER_TAG}>`;
|
||||
|
||||
const createPlaceholder = (idx: number | string) =>
|
||||
`${MD_PLACEHOLDER_OPEN}${idx}${MD_PLACEHOLDER_CLOSE}`;
|
||||
|
||||
type ParseFragment =
|
||||
| string
|
||||
| VNode<
|
||||
RendererNode,
|
||||
RendererElement,
|
||||
{
|
||||
[key: string]: any;
|
||||
}
|
||||
>
|
||||
| (
|
||||
| string
|
||||
| VNode<
|
||||
RendererNode,
|
||||
RendererElement,
|
||||
{
|
||||
[key: string]: any;
|
||||
}
|
||||
>
|
||||
| undefined
|
||||
)[];
|
||||
|
||||
const parseMd = (src: string) => {
|
||||
let i = 0;
|
||||
const result: string[] = [];
|
||||
|
||||
const checkChar = (c: "~" | "*" | "_" | "`", double: boolean) => {
|
||||
const n = double ? 2 : 1;
|
||||
|
||||
if (src[i] === c && (double ? src[i + 1] === c : true)) {
|
||||
const end = src.indexOf(c.repeat(n), i + n);
|
||||
const className = {
|
||||
"~": "strikethrough",
|
||||
"*": double ? "bold" : "italic",
|
||||
_: double ? "bold" : "italic",
|
||||
"`": "monospace",
|
||||
}[c];
|
||||
|
||||
if (end !== -1) {
|
||||
result.push(
|
||||
`<span class='irc-${className}'>${parseMd(src.slice(i + n, end))}</span>`
|
||||
);
|
||||
i = end + n;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (i < src.length) {
|
||||
checkChar("*", true);
|
||||
checkChar("*", false);
|
||||
checkChar("_", true);
|
||||
checkChar("_", false);
|
||||
checkChar("~", true);
|
||||
checkChar("`", false);
|
||||
|
||||
if (src[i]) {
|
||||
if (src[i] === "<") {
|
||||
if (src.slice(i, i + 31) === MD_PLACEHOLDER_OPEN) {
|
||||
let j = -1;
|
||||
|
||||
if ((j = src.indexOf(MD_PLACEHOLDER_CLOSE, i)) !== -1) {
|
||||
const idx = src.slice(i + MD_PLACEHOLDER_OPEN.length, j);
|
||||
let placeholder = createPlaceholder(idx);
|
||||
|
||||
i += placeholder.length - 1;
|
||||
const NUMERIC_TEST = /^\d(\.\d+)?$/;
|
||||
|
||||
if (!idx.match(NUMERIC_TEST)) {
|
||||
placeholder = placeholder
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
result.push(placeholder);
|
||||
} else {
|
||||
result.push("<");
|
||||
}
|
||||
} else {
|
||||
result.push("<");
|
||||
}
|
||||
} else {
|
||||
result.push(src[i]);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return "".concat(...result);
|
||||
};
|
||||
|
||||
type DomElementRepr = {
|
||||
content: (DomElementRepr | string)[];
|
||||
type?: string;
|
||||
attributes?: Record<string, string>;
|
||||
};
|
||||
|
||||
function mapDOM(element: HTMLElement | string): DomElementRepr {
|
||||
const treeObject: any = {};
|
||||
let docNode: Document | null = null;
|
||||
|
||||
// If string convert to document Node
|
||||
if (typeof element === "string") {
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser();
|
||||
docNode = parser.parseFromString(element, "text/html");
|
||||
}
|
||||
|
||||
if (!docNode) {
|
||||
throw new Error("Error parsing XML");
|
||||
}
|
||||
|
||||
element = docNode.querySelector("body")!;
|
||||
}
|
||||
|
||||
// Recursively loop through DOM elements and assign properties to object
|
||||
function treeHTML(elt: any, object: any) {
|
||||
object.type = elt.nodeName;
|
||||
const nodeList = elt.childNodes;
|
||||
|
||||
if (nodeList !== null) {
|
||||
if (nodeList.length) {
|
||||
object.content = [];
|
||||
|
||||
for (let i = 0; i < nodeList.length; i++) {
|
||||
if (nodeList[i].nodeType === 3) {
|
||||
object.content.push(nodeList[i].nodeValue);
|
||||
} else {
|
||||
object.content.push({});
|
||||
treeHTML(nodeList[i], object.content[object.content.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elt?.attributes) {
|
||||
if (elt.attributes.length) {
|
||||
object.attributes = {};
|
||||
|
||||
for (let i = 0; i < elt.attributes.length; i++) {
|
||||
object.attributes[elt.attributes[i].nodeName] = elt.attributes[i].nodeValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
treeHTML(element, treeObject);
|
||||
|
||||
return treeObject as DomElementRepr;
|
||||
}
|
||||
|
||||
const rehydrate = (parsed: string, htmls: Map<number, ParseFragment>) => {
|
||||
const parsedDom = mapDOM(parsed);
|
||||
const result: (ParseFragment | string)[] = [];
|
||||
|
||||
const create = (content: typeof parsedDom.content) => {
|
||||
const children: typeof result = [];
|
||||
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const item of content) {
|
||||
if (typeof item === "string") {
|
||||
children.push(item);
|
||||
} else if (item?.type) {
|
||||
if (item.type.toLowerCase() === MD_PLACEHOLDER_TAG) {
|
||||
if (!item?.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const elt = htmls.get(parseFloat(item.content[0] as string));
|
||||
|
||||
if (!elt) {
|
||||
children.push(createPlaceholder(item.content[0] as string));
|
||||
} else {
|
||||
children.push(elt);
|
||||
}
|
||||
} else {
|
||||
children.push(
|
||||
h(
|
||||
item.type,
|
||||
{...item.attributes},
|
||||
...(item?.content ? item.content.map((elt) => [elt]).map(create) : [])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
result.push(...create(parsedDom.content));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ParsedMessage",
|
||||
functional: true,
|
||||
|
@ -232,7 +35,7 @@ export default defineComponent({
|
|||
|
||||
if (context?.message?.type === "message" && this.store.state.settings.parseMd) {
|
||||
const htmls: Map<number, ParseFragment> = new Map();
|
||||
const ids = new Set<number>();
|
||||
const id = idGenerator();
|
||||
|
||||
const generateStandIns = (nodes) => {
|
||||
const result: string[] = [];
|
||||
|
@ -244,16 +47,10 @@ export default defineComponent({
|
|||
if (typeof nodes[i] === "string") {
|
||||
result.push(nodes[i]);
|
||||
} else {
|
||||
let id = Math.random();
|
||||
const nextId = id();
|
||||
|
||||
while (ids.has(id)) {
|
||||
id = Math.random();
|
||||
}
|
||||
|
||||
ids.add(id);
|
||||
|
||||
htmls.set(id, nodes[i]);
|
||||
result.push(createPlaceholder(id));
|
||||
htmls.set(nextId, nodes[i]);
|
||||
result.push(createPlaceholder(nextId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
221
client/js/helpers/mdHelpers.ts
Normal file
221
client/js/helpers/mdHelpers.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
import {VNode, RendererNode, RendererElement, h} from "vue";
|
||||
|
||||
const MD_PLACEHOLDER_TAG = "thelounge-mdparse-placeholder";
|
||||
const MD_PLACEHOLDER_OPEN = `<${MD_PLACEHOLDER_TAG}>`;
|
||||
const MD_PLACEHOLDER_CLOSE = `</${MD_PLACEHOLDER_TAG}>`;
|
||||
|
||||
export const createPlaceholder = (idx: number | string) =>
|
||||
`${MD_PLACEHOLDER_OPEN}${idx}${MD_PLACEHOLDER_CLOSE}`;
|
||||
|
||||
export type ParseFragment =
|
||||
| string
|
||||
| VNode<
|
||||
RendererNode,
|
||||
RendererElement,
|
||||
{
|
||||
[key: string]: any;
|
||||
}
|
||||
>
|
||||
| (
|
||||
| string
|
||||
| VNode<
|
||||
RendererNode,
|
||||
RendererElement,
|
||||
{
|
||||
[key: string]: any;
|
||||
}
|
||||
>
|
||||
| undefined
|
||||
)[];
|
||||
|
||||
export const parseMd = (src: string) => {
|
||||
let i = 0;
|
||||
const result: string[] = [];
|
||||
|
||||
const checkChar = (c: "~" | "*" | "_" | "`", double: boolean) => {
|
||||
const n = double ? 2 : 1;
|
||||
|
||||
if (src[i] === c && (double ? src[i + 1] === c : true)) {
|
||||
const end = src.indexOf(c.repeat(n), i + n);
|
||||
const className = {
|
||||
"~": "strikethrough",
|
||||
"*": double ? "bold" : "italic",
|
||||
_: double ? "bold" : "italic",
|
||||
"`": "monospace",
|
||||
}[c];
|
||||
|
||||
if (end !== -1) {
|
||||
result.push(
|
||||
`<span class='irc-${className}'>${parseMd(src.slice(i + n, end))}</span>`
|
||||
);
|
||||
i = end + n;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (i < src.length) {
|
||||
checkChar("*", true);
|
||||
checkChar("*", false);
|
||||
checkChar("_", true);
|
||||
checkChar("_", false);
|
||||
checkChar("~", true);
|
||||
checkChar("`", false);
|
||||
|
||||
if (src[i]) {
|
||||
if (src[i] === "<") {
|
||||
if (src.slice(i, i + MD_PLACEHOLDER_OPEN.length) === MD_PLACEHOLDER_OPEN) {
|
||||
let j = -1;
|
||||
|
||||
if ((j = src.indexOf(MD_PLACEHOLDER_CLOSE, i)) !== -1) {
|
||||
const idx = src.slice(i + MD_PLACEHOLDER_OPEN.length, j);
|
||||
let placeholder = createPlaceholder(idx);
|
||||
|
||||
i += placeholder.length - 1;
|
||||
const NUMERIC_TEST = /^\d(\.\d+)?$/;
|
||||
|
||||
if (!idx.match(NUMERIC_TEST)) {
|
||||
placeholder = placeholder
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
result.push(placeholder);
|
||||
} else {
|
||||
result.push("<");
|
||||
}
|
||||
} else {
|
||||
result.push("<");
|
||||
}
|
||||
} else {
|
||||
result.push(src[i]);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return "".concat(...result);
|
||||
};
|
||||
|
||||
type DomElementRepr = {
|
||||
content: (DomElementRepr | string)[];
|
||||
type?: string;
|
||||
attributes?: Record<string, string>;
|
||||
};
|
||||
|
||||
function mapDOM(element: HTMLElement | string): DomElementRepr {
|
||||
const treeObject: any = {};
|
||||
let docNode: Document | null = null;
|
||||
|
||||
// If string convert to document Node
|
||||
if (typeof element === "string") {
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser();
|
||||
docNode = parser.parseFromString(element, "text/html");
|
||||
}
|
||||
|
||||
if (!docNode) {
|
||||
throw new Error("Error parsing XML");
|
||||
}
|
||||
|
||||
element = docNode.querySelector("body")!;
|
||||
}
|
||||
|
||||
// Recursively loop through DOM elements and assign properties to object
|
||||
function treeHTML(elt: any, object: any) {
|
||||
object.type = elt.nodeName;
|
||||
const nodeList = elt.childNodes;
|
||||
|
||||
if (nodeList !== null) {
|
||||
if (nodeList.length) {
|
||||
object.content = [];
|
||||
|
||||
for (let i = 0; i < nodeList.length; i++) {
|
||||
if (nodeList[i].nodeType === 3) {
|
||||
object.content.push(nodeList[i].nodeValue);
|
||||
} else {
|
||||
object.content.push({});
|
||||
treeHTML(nodeList[i], object.content[object.content.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elt?.attributes) {
|
||||
if (elt.attributes.length) {
|
||||
object.attributes = {};
|
||||
|
||||
for (let i = 0; i < elt.attributes.length; i++) {
|
||||
object.attributes[elt.attributes[i].nodeName] = elt.attributes[i].nodeValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
treeHTML(element, treeObject);
|
||||
|
||||
return treeObject as DomElementRepr;
|
||||
}
|
||||
|
||||
export const rehydrate = (parsed: string, htmls: Map<number, ParseFragment>) => {
|
||||
const parsedDom = mapDOM(parsed);
|
||||
const result: (ParseFragment | string)[] = [];
|
||||
|
||||
const create = (content: typeof parsedDom.content) => {
|
||||
const children: typeof result = [];
|
||||
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const item of content) {
|
||||
if (typeof item === "string") {
|
||||
children.push(item);
|
||||
} else if (item?.type) {
|
||||
if (item.type.toLowerCase() === MD_PLACEHOLDER_TAG) {
|
||||
if (!item?.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const elt = htmls.get(parseFloat(item.content[0] as string));
|
||||
|
||||
if (!elt) {
|
||||
children.push(createPlaceholder(item.content[0] as string));
|
||||
} else {
|
||||
children.push(elt);
|
||||
}
|
||||
} else {
|
||||
children.push(
|
||||
h(
|
||||
item.type,
|
||||
{...item.attributes},
|
||||
...(item?.content ? item.content.map((elt) => [elt]).map(create) : [])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
result.push(...create(parsedDom.content));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const idGenerator = () => {
|
||||
const ids = new Set();
|
||||
|
||||
const genId = () => {
|
||||
let id = Math.random();
|
||||
|
||||
while (ids.has(id)) {
|
||||
id = Math.random();
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
return genId;
|
||||
};
|
Loading…
Reference in a new issue