wip: updated markdown parsing for starred spans, improved error handling behavior of i18n messages
This commit is contained in:
parent
00d7e1afc7
commit
f7a504d5a3
3 changed files with 380 additions and 214 deletions
|
|
@ -16,9 +16,9 @@ const props = defineProps<{
|
||||||
lineClass?: CssClass;
|
lineClass?: CssClass;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const providedSlots =
|
const providedSlots =
|
||||||
defineSlots<{ [K in DeepReadonly<Slot>]: () => VNode[] }>();
|
defineSlots<{ [K in DeepReadonly<Slot>]: () => VNode[] }>();
|
||||||
console.log(Object.entries(props.markdown));
|
|
||||||
|
|
||||||
function tryResolveComponentName(type: unknown): string | undefined {
|
function tryResolveComponentName(type: unknown): string | undefined {
|
||||||
if (!type || typeof type !== "object") return undefined;
|
if (!type || typeof type !== "object") return undefined;
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,19 @@ export function compileLocale<Config extends LocaleConfig>(
|
||||||
return fallbackValue as GenericMessageFn;
|
return fallbackValue as GenericMessageFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => `[#${fmtMessageIdChain(valueMessageIdChain)}#]`;
|
switch (configValue[configMessageTypeSymbol]) {
|
||||||
|
case configStringSymbol: {
|
||||||
|
return () =>
|
||||||
|
createMissingStringFallback(valueMessageIdChain);
|
||||||
|
}
|
||||||
|
case configMarkdownSymbol: {
|
||||||
|
return () =>
|
||||||
|
createMissingMarkdownFallback(
|
||||||
|
valueMessageIdChain,
|
||||||
|
Object.keys(configValue.features.slots),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const uncompiledSubrecord = (() => {
|
const uncompiledSubrecord = (() => {
|
||||||
if (uncompiledValue?.type !== "subrecord") {
|
if (uncompiledValue?.type !== "subrecord") {
|
||||||
|
|
@ -298,33 +310,50 @@ function compileStringMessage(
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: (args: Record<string, FluentVariable> = {}) => {
|
ok: (args: Record<string, FluentVariable> = {}) => {
|
||||||
const stringLiteralRes = parseMessageLiteral(
|
const stringRes = ((): Result<string, string> => {
|
||||||
"string",
|
const stringLiteralRes = parseMessageLiteral(
|
||||||
bundle.formatPattern(pattern, args),
|
"string",
|
||||||
);
|
bundle.formatPattern(pattern, args),
|
||||||
|
|
||||||
if (stringLiteralRes.type === "err") {
|
|
||||||
// This should hopefully never happen since we've already
|
|
||||||
// verified all message variants parse as valid strings above
|
|
||||||
throw new Error(
|
|
||||||
`Failed to parse string literal after compilation!\n${stringLiteralRes.err}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (stringLiteralRes.type === "err") {
|
||||||
|
// This should hopefully never happen since we've already
|
||||||
|
// verified all message variants parse as valid strings above
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: `Failed to parse string literal after compilation!\n${stringLiteralRes.err}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringLiteral = stringLiteralRes.ok;
|
||||||
|
|
||||||
|
const res = parseString(stringLiteral);
|
||||||
|
|
||||||
|
if (res.type === "err") {
|
||||||
|
// This should hopefully never happen since we've already
|
||||||
|
// verified all message variants parse as valid strings above
|
||||||
|
// TODO: no we dont, do that
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: `Failed to parse string after compilation!\n${res.err}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const string = res.ok;
|
||||||
|
return { type: "ok", ok: string };
|
||||||
|
})();
|
||||||
|
|
||||||
|
switch (stringRes.type) {
|
||||||
|
case "ok": {
|
||||||
|
const string = stringRes.ok;
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
case "err": {
|
||||||
|
const error = stringRes.err;
|
||||||
|
console.error(error);
|
||||||
|
return createMissingStringFallback(messageIdChain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringLiteral = stringLiteralRes.ok;
|
|
||||||
|
|
||||||
const res = parseString(stringLiteral);
|
|
||||||
|
|
||||||
if (res.type === "err") {
|
|
||||||
// This should hopefully never happen since we've already
|
|
||||||
// verified all message variants parse as valid strings above
|
|
||||||
// TODO: no we dont, do that
|
|
||||||
throw new Error(
|
|
||||||
`Failed to parse string after compilation!\n${res.err}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.ok;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -345,7 +374,7 @@ function compileMarkdownMessage(
|
||||||
|
|
||||||
// typecheck markdown
|
// typecheck markdown
|
||||||
|
|
||||||
const markdownSlots = configMarkdown.features.slots;
|
const markdownSlots = Object.keys(configMarkdown.features.slots);
|
||||||
|
|
||||||
// check if all variants are valid markdown
|
// check if all variants are valid markdown
|
||||||
for (const variant of allVariants) {
|
for (const variant of allVariants) {
|
||||||
|
|
@ -359,10 +388,7 @@ function compileMarkdownMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownLiteral = markdownLiteralRes.ok;
|
const markdownLiteral = markdownLiteralRes.ok;
|
||||||
const markdownRes = parseMarkdown(
|
const markdownRes = parseMarkdown(markdownLiteral, markdownSlots);
|
||||||
markdownLiteral,
|
|
||||||
Object.keys(markdownSlots),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (markdownRes.type === "err") {
|
if (markdownRes.type === "err") {
|
||||||
return {
|
return {
|
||||||
|
|
@ -375,39 +401,83 @@ function compileMarkdownMessage(
|
||||||
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: (args: Record<string, FluentVariable> = {}) => {
|
ok: (args: Record<string, FluentVariable> = {}): Markdown => {
|
||||||
const markdownLiteralRes = parseMessageLiteral(
|
const markdownRes = ((): Result<Markdown, string> => {
|
||||||
"md",
|
const markdownLiteralRes = parseMessageLiteral(
|
||||||
bundle.formatPattern(pattern, args),
|
"md",
|
||||||
);
|
bundle.formatPattern(pattern, args),
|
||||||
|
|
||||||
if (markdownLiteralRes.type === "err") {
|
|
||||||
// This should hopefully never happen since we've already
|
|
||||||
// verified all message variants parse as valid markdown above
|
|
||||||
throw new Error(
|
|
||||||
`Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (markdownLiteralRes.type === "err") {
|
||||||
|
// This should hopefully never happen since we've already
|
||||||
|
// verified all message variants parse as valid markdown above
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: `Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdownLiteral = markdownLiteralRes.ok;
|
||||||
|
const res = parseMarkdown(markdownLiteral, markdownSlots);
|
||||||
|
|
||||||
|
if (res.type === "err") {
|
||||||
|
// This should hopefully never happen since we've already
|
||||||
|
// verified all message variants parse as valid markdown above
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: `Failed to parse markdown after compilation!\n${res.err}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "ok", ok: res.ok };
|
||||||
|
})();
|
||||||
|
|
||||||
|
switch (markdownRes.type) {
|
||||||
|
case "ok": {
|
||||||
|
const markdown = markdownRes.ok;
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
case "err": {
|
||||||
|
const error = markdownRes.err;
|
||||||
|
console.error(error);
|
||||||
|
return createMissingMarkdownFallback(
|
||||||
|
messageIdChain,
|
||||||
|
markdownSlots,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownLiteral = markdownLiteralRes.ok;
|
|
||||||
const res = parseMarkdown(
|
|
||||||
markdownLiteral,
|
|
||||||
Object.keys(markdownSlots),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.type === "err") {
|
|
||||||
// This should hopefully never happen since we've already
|
|
||||||
// verified all message variants parse as valid markdown above
|
|
||||||
throw new Error(
|
|
||||||
`Failed to parse markdown after compilation!\n${res.err}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.ok;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMissingStringFallback(
|
||||||
|
messageIdChain: readonly [...string[], string],
|
||||||
|
): string {
|
||||||
|
return `[#${fmtMessageIdChain(messageIdChain)}#]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMissingMarkdownFallback<Slot extends string>(
|
||||||
|
messageIdChain: readonly [...string[], string],
|
||||||
|
slots: Slot[],
|
||||||
|
): Markdown<Slot> {
|
||||||
|
return {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
paragraph: {
|
||||||
|
spans: [
|
||||||
|
{
|
||||||
|
type: "plain",
|
||||||
|
plain: createMissingStringFallback(messageIdChain),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
slots,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
|
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
|
||||||
subconfig: Subconfig;
|
subconfig: Subconfig;
|
||||||
uncompiledSublocale: UncompiledLocale | undefined;
|
uncompiledSublocale: UncompiledLocale | undefined;
|
||||||
|
|
|
||||||
|
|
@ -144,44 +144,32 @@ function parseMarkdownLine<Slot extends string>(
|
||||||
line: string,
|
line: string,
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
): Result<MarkdownLine<Slot>, string> {
|
): Result<MarkdownLine<Slot>, string> {
|
||||||
if (line.startsWith("#")) {
|
interface ResolvedLine {
|
||||||
const elementsRes = parseMarkdownSpans(line.substring(1), slots);
|
deprefixedLine: string;
|
||||||
|
type: MarkdownLine["type"];
|
||||||
if (elementsRes.type === "err") {
|
|
||||||
return {
|
|
||||||
type: "err",
|
|
||||||
err: `While parsing header spans:\n${elementsRes.err}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = elementsRes.ok;
|
|
||||||
return { type: "ok", ok: { type: "header", spans: elements } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.startsWith("-")) {
|
const { deprefixedLine, type } = ((): ResolvedLine => {
|
||||||
const elementsRes = parseMarkdownSpans(line.substring(1), slots);
|
if (line.startsWith("#")) {
|
||||||
|
return { deprefixedLine: line.substring(1), type: "header" };
|
||||||
if (elementsRes.type === "err") {
|
} else if (line.startsWith("-")) {
|
||||||
return {
|
return { deprefixedLine: line.substring(1), type: "ulistItem" };
|
||||||
type: "err",
|
} else {
|
||||||
err: `While parsing ulist item spans:\n${elementsRes.err}`,
|
return { deprefixedLine: line, type: "paragraph" };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const elements = elementsRes.ok;
|
const spansRes = parseMarkdownSpans(deprefixedLine, slots);
|
||||||
return { type: "ok", ok: { type: "ulistItem", spans: elements } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementsRes = parseMarkdownSpans(line, slots);
|
if (spansRes.type === "err") {
|
||||||
if (elementsRes.type === "err") {
|
|
||||||
return {
|
return {
|
||||||
type: "err",
|
type: "err",
|
||||||
err: `While parsing elements:\n${elementsRes.err}`,
|
err: `While parsing ${type} spans:\n${spansRes.err}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = elementsRes.ok;
|
const spans = spansRes.ok;
|
||||||
return { type: "ok", ok: { type: "paragraph", spans: elements } };
|
return { type: "ok", ok: { type, spans } };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMarkdownSpans<Slot extends string>(
|
function parseMarkdownSpans<Slot extends string>(
|
||||||
|
|
@ -197,64 +185,123 @@ function parseMarkdownSpans<Slot extends string>(
|
||||||
return { type: "err", err: "Subheaders are not supported." };
|
return { type: "err", err: "Subheaders are not supported." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedLine = line.trim();
|
const chars = line.split("");
|
||||||
const chars = trimmedLine.split("");
|
const spansRes = readMarkdownSpans(
|
||||||
const elementsRes = readMarkdownSpans(chars, slots, {
|
chars,
|
||||||
inItalic: false,
|
slots,
|
||||||
inBold: false,
|
ParseMarkdownSpansManager.new(),
|
||||||
});
|
);
|
||||||
if (elementsRes.type === "err") {
|
|
||||||
return elementsRes;
|
if (spansRes.type === "err") {
|
||||||
|
return spansRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = elementsRes.ok;
|
const spans = spansRes.ok;
|
||||||
return { type: "ok", ok: elements };
|
|
||||||
|
return { type: "ok", ok: spans };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReadMarkdownSpanCtx {
|
class ParseMarkdownSpansManager {
|
||||||
inItalic: boolean;
|
private inItalic: boolean;
|
||||||
inBold: boolean;
|
private inBold: boolean;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.inItalic = false;
|
||||||
|
this.inBold = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static new(): ParseMarkdownSpansManager {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
|
|
||||||
|
public tryUseItalic<R>(f: () => Result<R, string>): Result<R, string> {
|
||||||
|
if (this.inItalic) {
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: "Cannot nest italic span (*) inside of another italic span",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inItalic = true;
|
||||||
|
|
||||||
|
const fRes = f();
|
||||||
|
if (fRes.type === "err") {
|
||||||
|
return fRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inItalic = false;
|
||||||
|
|
||||||
|
const fOk = fRes.ok;
|
||||||
|
return { type: "ok", ok: fOk };
|
||||||
|
}
|
||||||
|
|
||||||
|
public tryUseBold<R>(f: () => Result<R, string>): Result<R, string> {
|
||||||
|
if (this.inBold) {
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: "Cannot nest bold span (**) inside of another bold span",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inBold = true;
|
||||||
|
|
||||||
|
const fRes = f();
|
||||||
|
if (fRes.type === "err") {
|
||||||
|
return fRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inBold = false;
|
||||||
|
|
||||||
|
const fOk = fRes.ok;
|
||||||
|
return { type: "ok", ok: fOk };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpans<Slot extends string>(
|
function readMarkdownSpans<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
ctx: ReadMarkdownSpanCtx,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>[], string> {
|
||||||
const elements: MarkdownSpan<Slot>[] = [];
|
const spans: MarkdownSpan<Slot>[] = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
const elementRes = readMarkdownSpan(chars, slots, ctx);
|
const spanRes = readMarkdownSpan(chars, slots, manager);
|
||||||
if (elementRes.type === "err") {
|
if (spanRes.type === "err") {
|
||||||
return elementRes;
|
return spanRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = elementRes.ok;
|
const span = spanRes.ok;
|
||||||
if (element.length === 0) {
|
if (span === undefined) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(...element);
|
spans.push(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "ok", ok: elements };
|
return { type: "ok", ok: spans };
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpan<Slot extends string>(
|
function readMarkdownSpan<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
ctx: ReadMarkdownSpanCtx,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot> | undefined, string> {
|
||||||
const [firstChar, secondChar] = chars;
|
const [firstChar, secondChar, thirdChar] = chars;
|
||||||
if (firstChar === undefined) {
|
if (firstChar === undefined) {
|
||||||
return { type: "ok", ok: [] };
|
return { type: "ok", ok: undefined };
|
||||||
} else if (firstChar === "<") {
|
} else if (firstChar === "<") {
|
||||||
return readMarkdownSpanSlot(chars, slots);
|
return readMarkdownSpanSlot(chars, slots);
|
||||||
} else if (firstChar === "[") {
|
} else if (firstChar === "[") {
|
||||||
return readMarkdownSpanLink(chars, slots, ctx);
|
return readMarkdownSpanLink(chars, slots, manager);
|
||||||
} else if (firstChar === "*" && secondChar === "*" && !ctx.inBold) {
|
} else if (firstChar === "*") {
|
||||||
return readMarkdownSpanBold(chars, slots, ctx);
|
if (secondChar !== "*") {
|
||||||
} else if (firstChar === "*" && secondChar !== "*" && !ctx.inItalic) {
|
return readMarkdownSpanItalic(chars, slots, manager);
|
||||||
return readMarkdownSpanItalic(chars, slots, ctx);
|
}
|
||||||
|
|
||||||
|
if (thirdChar !== "*") {
|
||||||
|
return readMarkdownSpanBold(chars, slots, manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return readMarkdownSpanBoldItalic(chars, slots, manager);
|
||||||
} else {
|
} else {
|
||||||
return readMarkdownSpanPlain(chars);
|
return readMarkdownSpanPlain(chars);
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +315,7 @@ function isInArray<T, U extends T>(value: T, array: readonly U[]): value is U {
|
||||||
function readMarkdownSpanSlot<Slot extends string>(
|
function readMarkdownSpanSlot<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
const openAngleRes = expectReadChar(chars, "<");
|
const openAngleRes = expectReadChar(chars, "<");
|
||||||
if (openAngleRes.type === "err") {
|
if (openAngleRes.type === "err") {
|
||||||
return openAngleRes;
|
return openAngleRes;
|
||||||
|
|
@ -289,20 +336,20 @@ function readMarkdownSpanSlot<Slot extends string>(
|
||||||
return { type: "err", err: `Unexpected slot name: ${slotName}` };
|
return { type: "err", err: `Unexpected slot name: ${slotName}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "ok", ok: [{ type: "slot", slot: slotName }] };
|
return { type: "ok", ok: { type: "slot", slot: slotName } };
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpanLink<Slot extends string>(
|
function readMarkdownSpanLink<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
ctx: ReadMarkdownSpanCtx,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
const openSquareRes = expectReadChar(chars, "[");
|
const openSquareRes = expectReadChar(chars, "[");
|
||||||
if (openSquareRes.type === "err") {
|
if (openSquareRes.type === "err") {
|
||||||
return openSquareRes;
|
return openSquareRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelElementsRes = readMarkdownSpans(chars, slots, ctx);
|
const labelElementsRes = readMarkdownSpans(chars, slots, manager);
|
||||||
if (labelElementsRes.type === "err") {
|
if (labelElementsRes.type === "err") {
|
||||||
return labelElementsRes;
|
return labelElementsRes;
|
||||||
}
|
}
|
||||||
|
|
@ -493,9 +540,7 @@ function readMarkdownSpanLink<Slot extends string>(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: [
|
ok: { type: "link", link: { label: resolvedLabel, to: dest, newTab } },
|
||||||
{ type: "link", link: { label: resolvedLabel, to: dest, newTab } },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,106 +612,157 @@ function validateInternalDest(dest: string): Result<SmartInternalDest, string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpanBold<Slot extends string>(
|
|
||||||
chars: string[],
|
|
||||||
slots: readonly Slot[],
|
|
||||||
ctx: ReadMarkdownSpanCtx,
|
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
|
||||||
const firstStarRes = expectReadChar(chars, "*");
|
|
||||||
if (firstStarRes.type === "err") {
|
|
||||||
return firstStarRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondStarRes = expectReadChar(chars, "*");
|
|
||||||
if (secondStarRes.type === "err") {
|
|
||||||
return secondStarRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.inBold = true;
|
|
||||||
const elements: MarkdownSpan<Slot>[] = [];
|
|
||||||
let closed = false;
|
|
||||||
while (true) {
|
|
||||||
const elementRes = readMarkdownSpan(chars, slots, ctx);
|
|
||||||
if (elementRes.type === "err") {
|
|
||||||
return elementRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = elementRes.ok;
|
|
||||||
if (element.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
elements.push(...element);
|
|
||||||
const [firstChar, secondChar] = chars;
|
|
||||||
if (firstChar === "*" && secondChar === "*") {
|
|
||||||
chars.shift();
|
|
||||||
chars.shift();
|
|
||||||
closed = true;
|
|
||||||
break;
|
|
||||||
} else if (firstChar === undefined) {
|
|
||||||
closed = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.inBold = !closed;
|
|
||||||
if (closed) {
|
|
||||||
return { type: "ok", ok: [{ type: "bold", bold: elements }] };
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: "ok",
|
|
||||||
ok: [{ type: "plain", plain: "**" }, ...elements],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readMarkdownSpanItalic<Slot extends string>(
|
function readMarkdownSpanItalic<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: readonly Slot[],
|
||||||
ctx: ReadMarkdownSpanCtx,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
const starRes = expectReadChar(chars, "*");
|
return manager.tryUseItalic(() => {
|
||||||
if (starRes.type === "err") {
|
const singleStarRes = expectReadString(chars, "*");
|
||||||
return starRes;
|
if (singleStarRes.type === "err") {
|
||||||
}
|
return singleStarRes;
|
||||||
|
|
||||||
ctx.inItalic = true;
|
|
||||||
const elements: MarkdownSpan<Slot>[] = [];
|
|
||||||
let closed = false;
|
|
||||||
while (true) {
|
|
||||||
const elementRes = readMarkdownSpan(chars, slots, ctx);
|
|
||||||
if (elementRes.type === "err") {
|
|
||||||
return elementRes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = elementRes.ok;
|
const spans: MarkdownSpan<Slot>[] = [];
|
||||||
if (element.length === 0) {
|
let closed = false;
|
||||||
break;
|
while (true) {
|
||||||
|
const spanRes = readMarkdownSpan(chars, slots, manager);
|
||||||
|
if (spanRes.type === "err") {
|
||||||
|
return spanRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = spanRes.ok;
|
||||||
|
if (span === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.push(span);
|
||||||
|
|
||||||
|
const [firstChar] = chars;
|
||||||
|
if (firstChar === "*") {
|
||||||
|
chars.shift();
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
} else if (firstChar === undefined) {
|
||||||
|
closed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(...element);
|
if (!closed) {
|
||||||
const [firstChar] = chars;
|
return { type: "err", err: "Italic span (*) is never closed" };
|
||||||
if (firstChar === "*") {
|
|
||||||
chars.shift();
|
|
||||||
closed = true;
|
|
||||||
break;
|
|
||||||
} else if (firstChar === undefined) {
|
|
||||||
closed = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ctx.inItalic = !closed;
|
return { type: "ok", ok: { type: "italic", italic: spans } };
|
||||||
if (closed) {
|
});
|
||||||
return { type: "ok", ok: [{ type: "italic", italic: elements }] };
|
}
|
||||||
} else {
|
|
||||||
return { type: "ok", ok: [{ type: "plain", plain: "*" }, ...elements] };
|
function readMarkdownSpanBold<Slot extends string>(
|
||||||
}
|
chars: string[],
|
||||||
|
slots: readonly Slot[],
|
||||||
|
manager: ParseMarkdownSpansManager,
|
||||||
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
|
return manager.tryUseBold(() => {
|
||||||
|
const doubleStarRes = expectReadString(chars, "**");
|
||||||
|
if (doubleStarRes.type === "err") {
|
||||||
|
return doubleStarRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spans: MarkdownSpan<Slot>[] = [];
|
||||||
|
let closed = false;
|
||||||
|
while (true) {
|
||||||
|
const spanRes = readMarkdownSpan(chars, slots, manager);
|
||||||
|
if (spanRes.type === "err") {
|
||||||
|
return spanRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = spanRes.ok;
|
||||||
|
if (span === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.push(span);
|
||||||
|
const [firstChar, secondChar] = chars;
|
||||||
|
if (firstChar === "*" && secondChar === "*") {
|
||||||
|
chars.shift();
|
||||||
|
chars.shift();
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
} else if (firstChar === undefined) {
|
||||||
|
closed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!closed) {
|
||||||
|
return { type: "err", err: "Bold span (**) is never closed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "ok", ok: { type: "bold", bold: spans } };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMarkdownSpanBoldItalic<Slot extends string>(
|
||||||
|
chars: string[],
|
||||||
|
slots: readonly Slot[],
|
||||||
|
manager: ParseMarkdownSpansManager,
|
||||||
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
|
return manager.tryUseBold(() =>
|
||||||
|
manager.tryUseItalic(() => {
|
||||||
|
const tripleStarRes = expectReadString(chars, "***");
|
||||||
|
if (tripleStarRes.type === "err") {
|
||||||
|
return tripleStarRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spans: MarkdownSpan<Slot>[] = [];
|
||||||
|
let closed = false;
|
||||||
|
while (true) {
|
||||||
|
const spanRes = readMarkdownSpan(chars, slots, manager);
|
||||||
|
if (spanRes.type === "err") {
|
||||||
|
return spanRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = spanRes.ok;
|
||||||
|
if (span === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.push(span);
|
||||||
|
const [firstChar, secondChar, thirdChar] = chars;
|
||||||
|
if (
|
||||||
|
firstChar === "*"
|
||||||
|
&& secondChar === "*"
|
||||||
|
&& thirdChar === "*"
|
||||||
|
) {
|
||||||
|
chars.shift();
|
||||||
|
chars.shift();
|
||||||
|
chars.shift();
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
} else if (firstChar === undefined) {
|
||||||
|
closed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!closed) {
|
||||||
|
return {
|
||||||
|
type: "err",
|
||||||
|
err: "Bold italic span (***) is never closed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "ok",
|
||||||
|
ok: { type: "bold", bold: [{ type: "italic", italic: spans }] },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpanPlain<Slot extends string>(
|
function readMarkdownSpanPlain<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot> | undefined, string> {
|
||||||
let plain = "";
|
let plain = "";
|
||||||
let escaped = false;
|
let escaped = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -701,7 +797,7 @@ function readMarkdownSpanPlain<Slot extends string>(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: plain.length > 0 ? [{ type: "plain", plain }] : [],
|
ok: plain.length === 0 ? undefined : { type: "plain", plain },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue