2022-04-25 08:33:38 +00:00
|
|
|
import {
|
|
|
|
BlockContext,
|
2022-10-10 12:50:21 +00:00
|
|
|
Language,
|
2022-04-25 08:33:38 +00:00
|
|
|
LeafBlock,
|
|
|
|
LeafBlockParser,
|
2022-10-17 13:48:21 +00:00
|
|
|
Line,
|
2022-10-10 12:50:21 +00:00
|
|
|
markdown,
|
2022-04-25 08:33:38 +00:00
|
|
|
MarkdownConfig,
|
2022-10-17 13:48:21 +00:00
|
|
|
StreamLanguage,
|
2022-12-05 12:05:48 +00:00
|
|
|
Strikethrough,
|
2022-10-10 12:50:21 +00:00
|
|
|
styleTags,
|
|
|
|
tags as t,
|
2022-04-25 08:33:38 +00:00
|
|
|
TaskList,
|
2022-10-17 13:48:21 +00:00
|
|
|
yamlLanguage,
|
2022-12-14 19:04:20 +00:00
|
|
|
} from "../deps.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
import * as ct from "./customtags.ts";
|
2022-04-25 08:33:38 +00:00
|
|
|
import {
|
|
|
|
MDExt,
|
|
|
|
mdExtensionStyleTags,
|
|
|
|
mdExtensionSyntaxConfig,
|
2022-10-10 12:50:21 +00:00
|
|
|
} from "./markdown_ext.ts";
|
2022-03-20 08:56:28 +00:00
|
|
|
|
2022-11-27 07:48:01 +00:00
|
|
|
export const pageLinkRegex = /^\[\[([^\]\|]+)(\|([^\]]+))?\]\]/;
|
2022-04-08 15:46:09 +00:00
|
|
|
|
2022-03-20 08:56:28 +00:00
|
|
|
const WikiLink: MarkdownConfig = {
|
2022-11-29 08:11:23 +00:00
|
|
|
defineNodes: [
|
|
|
|
{ name: "WikiLink", style: ct.WikiLinkTag },
|
|
|
|
{ name: "WikiLinkPage", style: ct.WikiLinkPageTag },
|
|
|
|
{ name: "WikiLinkAlias", style: ct.WikiLinkPageTag },
|
|
|
|
{ name: "WikiLinkMark", style: t.processingInstruction },
|
|
|
|
],
|
2022-03-20 08:56:28 +00:00
|
|
|
parseInline: [
|
|
|
|
{
|
|
|
|
name: "WikiLink",
|
|
|
|
parse(cx, next, pos) {
|
|
|
|
let match: RegExpMatchArray | null;
|
|
|
|
if (
|
|
|
|
next != 91 /* '[' */ ||
|
2022-04-10 09:04:07 +00:00
|
|
|
!(match = pageLinkRegex.exec(cx.slice(pos, cx.end)))
|
2022-03-20 08:56:28 +00:00
|
|
|
) {
|
|
|
|
return -1;
|
|
|
|
}
|
2022-11-29 08:11:23 +00:00
|
|
|
const [fullMatch, page, pipePart, label] = match;
|
|
|
|
const endPos = pos + fullMatch.length;
|
2022-11-27 07:48:01 +00:00
|
|
|
let aliasElts: any[] = [];
|
|
|
|
if (pipePart) {
|
|
|
|
const pipeStartPos = pos + 2 + page.length;
|
|
|
|
aliasElts = [
|
|
|
|
cx.elt("WikiLinkMark", pipeStartPos, pipeStartPos + 1),
|
|
|
|
cx.elt(
|
|
|
|
"WikiLinkAlias",
|
|
|
|
pipeStartPos + 1,
|
|
|
|
pipeStartPos + 1 + label.length,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
2022-11-18 15:04:37 +00:00
|
|
|
return cx.addElement(
|
|
|
|
cx.elt("WikiLink", pos, endPos, [
|
|
|
|
cx.elt("WikiLinkMark", pos, pos + 2),
|
2022-11-27 07:48:01 +00:00
|
|
|
cx.elt("WikiLinkPage", pos + 2, pos + 2 + page.length),
|
|
|
|
...aliasElts,
|
2022-11-18 15:04:37 +00:00
|
|
|
cx.elt("WikiLinkMark", endPos - 2, endPos),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
after: "Emphasis",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2022-11-29 08:11:23 +00:00
|
|
|
export const commandLinkRegex = /^\{\[([^\]\|]+)(\|([^\]]+))?\]\}/;
|
2022-11-18 15:04:37 +00:00
|
|
|
|
|
|
|
const CommandLink: MarkdownConfig = {
|
|
|
|
defineNodes: [
|
|
|
|
{ name: "CommandLink", style: { "CommandLink/...": ct.CommandLinkTag } },
|
|
|
|
{ name: "CommandLinkName", style: ct.CommandLinkNameTag },
|
2022-11-29 08:11:23 +00:00
|
|
|
{ name: "CommandLinkAlias", style: ct.CommandLinkNameTag },
|
|
|
|
{ name: "CommandLinkMark", style: t.processingInstruction },
|
2022-11-18 15:04:37 +00:00
|
|
|
],
|
|
|
|
parseInline: [
|
|
|
|
{
|
|
|
|
name: "CommandLink",
|
|
|
|
parse(cx, next, pos) {
|
|
|
|
let match: RegExpMatchArray | null;
|
|
|
|
if (
|
|
|
|
next != 123 /* '{' */ ||
|
|
|
|
!(match = commandLinkRegex.exec(cx.slice(pos, cx.end)))
|
|
|
|
) {
|
|
|
|
return -1;
|
|
|
|
}
|
2022-11-29 08:11:23 +00:00
|
|
|
const [fullMatch, command, pipePart, label] = match;
|
|
|
|
const endPos = pos + fullMatch.length;
|
|
|
|
|
|
|
|
let aliasElts: any[] = [];
|
|
|
|
if (pipePart) {
|
|
|
|
const pipeStartPos = pos + 2 + command.length;
|
|
|
|
aliasElts = [
|
|
|
|
cx.elt("CommandLinkMark", pipeStartPos, pipeStartPos + 1),
|
|
|
|
cx.elt(
|
|
|
|
"CommandLinkAlias",
|
|
|
|
pipeStartPos + 1,
|
|
|
|
pipeStartPos + 1 + label.length,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
2022-03-20 08:56:28 +00:00
|
|
|
return cx.addElement(
|
2022-11-18 15:04:37 +00:00
|
|
|
cx.elt("CommandLink", pos, endPos, [
|
|
|
|
cx.elt("CommandLinkMark", pos, pos + 2),
|
2022-11-29 08:11:23 +00:00
|
|
|
cx.elt("CommandLinkName", pos + 2, pos + 2 + command.length),
|
|
|
|
...aliasElts,
|
2022-11-18 15:04:37 +00:00
|
|
|
cx.elt("CommandLinkMark", endPos - 2, endPos),
|
2022-10-10 12:50:21 +00:00
|
|
|
]),
|
2022-03-20 08:56:28 +00:00
|
|
|
);
|
|
|
|
},
|
|
|
|
after: "Emphasis",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2022-07-04 13:07:27 +00:00
|
|
|
const HighlightDelim = { resolve: "Highlight", mark: "HighlightMark" };
|
|
|
|
|
2022-12-05 12:05:48 +00:00
|
|
|
export const Highlight: MarkdownConfig = {
|
2022-07-04 13:07:27 +00:00
|
|
|
defineNodes: [
|
|
|
|
{
|
|
|
|
name: "Highlight",
|
|
|
|
style: { "Highlight/...": ct.Highlight },
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "HighlightMark",
|
|
|
|
style: t.processingInstruction,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
parseInline: [
|
|
|
|
{
|
|
|
|
name: "Highlight",
|
|
|
|
parse(cx, next, pos) {
|
|
|
|
if (next != 61 /* '=' */ || cx.char(pos + 1) != 61) return -1;
|
|
|
|
return cx.addDelimiter(HighlightDelim, pos, pos + 2, true, true);
|
|
|
|
},
|
|
|
|
after: "Emphasis",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2023-07-26 15:12:56 +00:00
|
|
|
export const attributeStartRegex = /^\[(\w+)(::?\s*)/;
|
2023-07-24 17:54:31 +00:00
|
|
|
|
|
|
|
export const Attribute: MarkdownConfig = {
|
|
|
|
defineNodes: [
|
|
|
|
{ name: "Attribute", style: { "Attribute/...": ct.AttributeTag } },
|
|
|
|
{ name: "AttributeName", style: ct.AttributeNameTag },
|
|
|
|
{ name: "AttributeValue", style: ct.AttributeValueTag },
|
|
|
|
{ name: "AttributeMark", style: t.processingInstruction },
|
|
|
|
{ name: "AttributeColon", style: t.processingInstruction },
|
|
|
|
],
|
|
|
|
parseInline: [
|
|
|
|
{
|
|
|
|
name: "Attribute",
|
|
|
|
parse(cx, next, pos) {
|
|
|
|
let match: RegExpMatchArray | null;
|
2023-07-26 15:12:56 +00:00
|
|
|
const textFromPos = cx.slice(pos, cx.end);
|
2023-07-24 17:54:31 +00:00
|
|
|
if (
|
|
|
|
next != 91 /* '[' */ ||
|
|
|
|
// and match the whole thing
|
2023-07-26 15:12:56 +00:00
|
|
|
!(match = attributeStartRegex.exec(textFromPos))
|
2023-07-24 17:54:31 +00:00
|
|
|
) {
|
|
|
|
return -1;
|
|
|
|
}
|
2023-07-26 15:12:56 +00:00
|
|
|
const [fullMatch, attributeName, attributeColon] = match;
|
|
|
|
const attributeValueStart = pos + fullMatch.length;
|
|
|
|
let bracketNestingDepth = 1;
|
|
|
|
let valueLength = fullMatch.length;
|
|
|
|
loopLabel:
|
|
|
|
for (; valueLength < textFromPos.length; valueLength++) {
|
|
|
|
switch (textFromPos[valueLength]) {
|
|
|
|
case "[":
|
|
|
|
bracketNestingDepth++;
|
|
|
|
break;
|
|
|
|
case "]":
|
|
|
|
bracketNestingDepth--;
|
|
|
|
if (bracketNestingDepth === 0) {
|
|
|
|
// Done!
|
|
|
|
break loopLabel;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (bracketNestingDepth !== 0) {
|
|
|
|
console.log("Failed to parse attribute", fullMatch, textFromPos);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (textFromPos[valueLength + 1] === "(") {
|
|
|
|
console.log("Link", fullMatch, textFromPos);
|
|
|
|
// This turns out to be a link, back out!
|
|
|
|
return -1;
|
|
|
|
}
|
2023-07-24 17:54:31 +00:00
|
|
|
|
|
|
|
return cx.addElement(
|
2023-07-26 15:12:56 +00:00
|
|
|
cx.elt("Attribute", pos, pos + valueLength + 1, [
|
2023-07-24 17:54:31 +00:00
|
|
|
cx.elt("AttributeMark", pos, pos + 1), // [
|
|
|
|
cx.elt("AttributeName", pos + 1, pos + 1 + attributeName.length),
|
|
|
|
cx.elt(
|
|
|
|
"AttributeColon",
|
|
|
|
pos + 1 + attributeName.length,
|
|
|
|
pos + 1 + attributeName.length + attributeColon.length,
|
|
|
|
),
|
|
|
|
cx.elt(
|
|
|
|
"AttributeValue",
|
|
|
|
pos + 1 + attributeName.length + attributeColon.length,
|
2023-07-26 15:12:56 +00:00
|
|
|
pos + valueLength,
|
2023-07-24 17:54:31 +00:00
|
|
|
),
|
2023-07-26 15:12:56 +00:00
|
|
|
cx.elt("AttributeMark", pos + valueLength, pos + valueLength + 1), // [
|
2023-07-24 17:54:31 +00:00
|
|
|
]),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
after: "Emphasis",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2022-03-20 08:56:28 +00:00
|
|
|
class CommentParser implements LeafBlockParser {
|
|
|
|
nextLine() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
finish(cx: BlockContext, leaf: LeafBlock) {
|
|
|
|
cx.addLeafElement(
|
|
|
|
leaf,
|
|
|
|
cx.elt("Comment", leaf.start, leaf.start + leaf.content.length, [
|
|
|
|
// cx.elt("CommentMarker", leaf.start, leaf.start + 3),
|
|
|
|
...cx.parser.parseInline(leaf.content.slice(3), leaf.start + 3),
|
2022-10-10 12:50:21 +00:00
|
|
|
]),
|
2022-03-20 08:56:28 +00:00
|
|
|
);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export const Comment: MarkdownConfig = {
|
|
|
|
defineNodes: [{ name: "Comment", block: true }],
|
|
|
|
parseBlock: [
|
|
|
|
{
|
|
|
|
name: "Comment",
|
2022-10-17 13:48:21 +00:00
|
|
|
leaf(_cx, leaf) {
|
2022-03-20 08:56:28 +00:00
|
|
|
return /^%%\s/.test(leaf.content) ? new CommentParser() : null;
|
|
|
|
},
|
|
|
|
after: "SetextHeading",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2022-12-14 19:04:20 +00:00
|
|
|
// Directive parser
|
|
|
|
|
|
|
|
const directiveStart = /^\s*<!--\s*#([a-z]+)\s*(.*?)-->\s*/;
|
|
|
|
const directiveEnd = /^\s*<!--\s*\/(.*?)-->\s*/;
|
|
|
|
|
|
|
|
import { parser as directiveParser } from "./parse-query.js";
|
2023-01-13 15:59:28 +00:00
|
|
|
import { Table } from "./table_parser.ts";
|
2022-12-14 19:04:20 +00:00
|
|
|
|
|
|
|
const highlightingDirectiveParser = directiveParser.configure({
|
|
|
|
props: [
|
|
|
|
styleTags({
|
|
|
|
"Name": t.variableName,
|
2022-12-19 12:42:20 +00:00
|
|
|
"String": t.string,
|
2022-12-14 19:04:20 +00:00
|
|
|
"Number": t.number,
|
2022-12-19 12:42:20 +00:00
|
|
|
"PageRef": ct.WikiLinkTag,
|
2022-12-14 19:04:20 +00:00
|
|
|
"Where Limit Select Render Order OrderDirection And": t.keyword,
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
export const Directive: MarkdownConfig = {
|
|
|
|
defineNodes: [
|
|
|
|
{ name: "Directive", block: true, style: ct.DirectiveTag },
|
|
|
|
{ name: "DirectiveStart", style: ct.DirectiveStartTag, block: true },
|
|
|
|
{ name: "DirectiveEnd", style: ct.DirectiveEndTag },
|
|
|
|
{ name: "DirectiveBody", block: true },
|
|
|
|
],
|
|
|
|
parseBlock: [{
|
|
|
|
name: "Directive",
|
|
|
|
parse: (cx, line: Line) => {
|
|
|
|
const match = directiveStart.exec(line.text);
|
|
|
|
if (!match) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// console.log("Parsing directive", line.text);
|
|
|
|
|
|
|
|
const frontStart = cx.parsedPos;
|
|
|
|
const [fullMatch, directive, arg] = match;
|
|
|
|
const elts = [];
|
|
|
|
if (directive === "query") {
|
|
|
|
const queryParseTree = highlightingDirectiveParser.parse(arg);
|
|
|
|
elts.push(cx.elt(
|
|
|
|
"DirectiveStart",
|
|
|
|
cx.parsedPos,
|
|
|
|
cx.parsedPos + line.text.length + 1,
|
|
|
|
[cx.elt(queryParseTree, frontStart + fullMatch.indexOf(arg))],
|
|
|
|
));
|
|
|
|
} else {
|
|
|
|
elts.push(cx.elt(
|
|
|
|
"DirectiveStart",
|
|
|
|
cx.parsedPos,
|
|
|
|
cx.parsedPos + line.text.length + 1,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// console.log("Query parse tree", queryParseTree.topNode);
|
|
|
|
|
|
|
|
cx.nextLine();
|
|
|
|
const startPos = cx.parsedPos;
|
|
|
|
let endPos = startPos;
|
|
|
|
let text = "";
|
|
|
|
let lastPos = cx.parsedPos;
|
|
|
|
let nesting = 0;
|
|
|
|
while (true) {
|
|
|
|
if (directiveEnd.exec(line.text) && nesting === 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
text += line.text + "\n";
|
|
|
|
endPos += line.text.length + 1;
|
|
|
|
if (directiveStart.exec(line.text)) {
|
|
|
|
nesting++;
|
|
|
|
}
|
|
|
|
if (directiveEnd.exec(line.text)) {
|
|
|
|
nesting--;
|
|
|
|
}
|
|
|
|
cx.nextLine();
|
|
|
|
if (cx.parsedPos === lastPos) {
|
|
|
|
// End of file, no progress made, there may be a better way to do this but :shrug:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
lastPos = cx.parsedPos;
|
|
|
|
}
|
|
|
|
const directiveBodyTree = cx.parser.parse(text);
|
|
|
|
|
|
|
|
elts.push(
|
|
|
|
cx.elt("DirectiveBody", startPos, endPos, [
|
|
|
|
cx.elt(directiveBodyTree, startPos),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
endPos = cx.parsedPos + line.text.length;
|
|
|
|
elts.push(cx.elt(
|
|
|
|
"DirectiveEnd",
|
|
|
|
cx.parsedPos,
|
|
|
|
cx.parsedPos + line.text.length,
|
|
|
|
));
|
|
|
|
cx.nextLine();
|
|
|
|
cx.addElement(cx.elt("Directive", frontStart, endPos, elts));
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
before: "HTMLBlock",
|
|
|
|
}],
|
|
|
|
};
|
|
|
|
|
2022-10-17 13:48:21 +00:00
|
|
|
// FrontMatter parser
|
|
|
|
|
2022-11-18 15:04:37 +00:00
|
|
|
const yamlLang = StreamLanguage.define(yamlLanguage);
|
2022-10-17 13:48:21 +00:00
|
|
|
|
|
|
|
export const FrontMatter: MarkdownConfig = {
|
|
|
|
defineNodes: [
|
|
|
|
{ name: "FrontMatter", block: true },
|
|
|
|
{ name: "FrontMatterMarker" },
|
|
|
|
{ name: "FrontMatterCode" },
|
|
|
|
],
|
|
|
|
parseBlock: [{
|
|
|
|
name: "FrontMatter",
|
|
|
|
parse: (cx, line: Line) => {
|
|
|
|
if (cx.parsedPos !== 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (line.text !== "---") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const frontStart = cx.parsedPos;
|
|
|
|
const elts = [
|
|
|
|
cx.elt(
|
|
|
|
"FrontMatterMarker",
|
|
|
|
cx.parsedPos,
|
|
|
|
cx.parsedPos + line.text.length + 1,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
cx.nextLine();
|
|
|
|
const startPos = cx.parsedPos;
|
|
|
|
let endPos = startPos;
|
|
|
|
let text = "";
|
|
|
|
let lastPos = cx.parsedPos;
|
|
|
|
do {
|
|
|
|
text += line.text + "\n";
|
|
|
|
endPos += line.text.length + 1;
|
|
|
|
cx.nextLine();
|
|
|
|
if (cx.parsedPos === lastPos) {
|
|
|
|
// End of file, no progress made, there may be a better way to do this but :shrug:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
lastPos = cx.parsedPos;
|
|
|
|
} while (line.text !== "---");
|
2022-11-18 15:04:37 +00:00
|
|
|
const yamlTree = yamlLang.parser.parse(text);
|
2022-10-17 13:48:21 +00:00
|
|
|
|
|
|
|
elts.push(
|
|
|
|
cx.elt("FrontMatterCode", startPos, endPos, [
|
|
|
|
cx.elt(yamlTree, startPos),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
endPos = cx.parsedPos + line.text.length;
|
|
|
|
elts.push(cx.elt(
|
|
|
|
"FrontMatterMarker",
|
|
|
|
cx.parsedPos,
|
|
|
|
cx.parsedPos + line.text.length,
|
|
|
|
));
|
|
|
|
cx.nextLine();
|
|
|
|
cx.addElement(cx.elt("FrontMatter", frontStart, endPos, elts));
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
before: "HorizontalRule",
|
|
|
|
}],
|
|
|
|
};
|
|
|
|
|
2022-04-11 18:34:09 +00:00
|
|
|
export default function buildMarkdown(mdExtensions: MDExt[]): Language {
|
2022-09-13 06:41:01 +00:00
|
|
|
return markdown({
|
|
|
|
extensions: [
|
2022-04-11 18:34:09 +00:00
|
|
|
WikiLink,
|
2022-11-18 15:04:37 +00:00
|
|
|
CommandLink,
|
2023-07-24 17:54:31 +00:00
|
|
|
Attribute,
|
2022-10-17 13:48:21 +00:00
|
|
|
FrontMatter,
|
2022-12-14 19:04:20 +00:00
|
|
|
Directive,
|
2022-04-11 18:34:09 +00:00
|
|
|
TaskList,
|
|
|
|
Comment,
|
2022-12-05 12:05:48 +00:00
|
|
|
Highlight,
|
2022-07-04 13:07:27 +00:00
|
|
|
Strikethrough,
|
2022-04-20 08:56:43 +00:00
|
|
|
Table,
|
2022-04-11 18:34:09 +00:00
|
|
|
...mdExtensions.map(mdExtensionSyntaxConfig),
|
2022-10-17 13:48:21 +00:00
|
|
|
|
2022-04-11 18:34:09 +00:00
|
|
|
{
|
|
|
|
props: [
|
|
|
|
styleTags({
|
|
|
|
Task: ct.TaskTag,
|
|
|
|
TaskMarker: ct.TaskMarkerTag,
|
|
|
|
Comment: ct.CommentTag,
|
2022-04-20 08:56:43 +00:00
|
|
|
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark":
|
|
|
|
t.processingInstruction,
|
|
|
|
"TableHeader/...": t.heading,
|
|
|
|
TableCell: t.content,
|
2022-04-24 16:06:34 +00:00
|
|
|
CodeInfo: ct.CodeInfoTag,
|
2022-08-29 14:48:52 +00:00
|
|
|
HorizontalRule: ct.HorizontalRuleTag,
|
2022-04-11 18:34:09 +00:00
|
|
|
}),
|
|
|
|
...mdExtensions.map((mdExt) =>
|
|
|
|
styleTags(mdExtensionStyleTags(mdExt))
|
|
|
|
),
|
|
|
|
],
|
|
|
|
},
|
2022-09-13 06:41:01 +00:00
|
|
|
],
|
|
|
|
}).language;
|
2022-04-11 18:34:09 +00:00
|
|
|
}
|