embedded-typescript
Version:
Type safe TypeScript templates
148 lines (147 loc) • 5.01 kB
JavaScript
function isPresent(idx) {
return idx !== -1;
}
const SYMBOLS = {
Header: "---",
Open: "<%",
Close: "%>",
Expression: "=",
};
function isExpression(token) {
return token.startsWith(SYMBOLS.Expression);
}
function stripModifierToken(token) {
let stripped = token;
if (isExpression(token)) {
stripped = stripped.slice(1);
}
return stripped;
}
export function isParseError(parsed) {
return typeof parsed === "object" && parsed != null && "error" in parsed;
}
function lineAndColumn(template, index) {
const lines = template.slice(0, index).split("\n");
const line = lines.length;
const column = (lines.pop()?.length ?? 0) + 1;
return {
line,
column,
};
}
function formatContext(template, position) {
const templateLines = template.split("\n").length - 1;
const hasMoreLines = templateLines > position.line;
const line = template.split("\n")[position.line - 1];
return ` |
${position.line.toString().padEnd(4, " ")}| ${line}
| ${"^".padStart(position.column, " ")}
| ${"|".padStart(position.column, " ")}
${hasMoreLines ? "..." : ""}
`;
}
function parseError({ error, template, startIdx, endIdx, }) {
const start = lineAndColumn(template, startIdx);
const end = lineAndColumn(template, endIdx);
return {
error,
position: {
start,
end,
},
context: formatContext(template, start),
};
}
export function parse(template) {
const parsed = [];
let position = 0;
// header
const headerStartIdx = template.indexOf(SYMBOLS.Header, 0);
if (!isPresent(headerStartIdx)) {
return parseError({
error: `Expected to find a 'Header' ('${SYMBOLS.Header}') in the template`,
template,
startIdx: 0,
endIdx: template.length - 1,
});
}
const headerEndIdx = template.indexOf(SYMBOLS.Header, headerStartIdx + SYMBOLS.Header.length);
if (!isPresent(headerEndIdx)) {
return parseError({
error: `Expected to find corresponding close to 'Header' ('${SYMBOLS.Header}') before end of template`,
template,
startIdx: headerStartIdx,
endIdx: template.length - 1,
});
}
const contentBeforeHeader = template.slice(0, headerStartIdx);
const nonWhiteSpaceIdx = contentBeforeHeader.search(/\S/);
if (isPresent(nonWhiteSpaceIdx)) {
return parseError({
error: `Unexpected token before 'Header' ('${SYMBOLS.Header}')`,
template,
startIdx: nonWhiteSpaceIdx,
endIdx: headerStartIdx,
});
}
parsed.push({
type: "header",
content: template.slice(headerStartIdx + SYMBOLS.Header.length, headerEndIdx),
});
position = headerEndIdx + SYMBOLS.Header.length;
// body
while (position < template.length) {
const openIdx = template.indexOf(SYMBOLS.Open, position);
const closeIdx = template.indexOf(SYMBOLS.Close, position);
if ((!isPresent(openIdx) && isPresent(closeIdx)) ||
(isPresent(openIdx) && isPresent(closeIdx) && closeIdx < openIdx)) {
return parseError({
error: `Unexpected closing tag '${SYMBOLS.Close}'`,
template,
startIdx: closeIdx,
endIdx: closeIdx + SYMBOLS.Close.length - 1,
});
}
if (isPresent(openIdx) && !isPresent(closeIdx)) {
return parseError({
error: `Expected to find corresponding closing tag '${SYMBOLS.Close}' before end of template`,
template,
startIdx: openIdx,
endIdx: template.length - 1,
});
}
const nextOpenIdx = template.indexOf(SYMBOLS.Open, openIdx + SYMBOLS.Open.length);
if (isPresent(nextOpenIdx) && nextOpenIdx < closeIdx) {
return parseError({
error: `Unexpected opening tag '${SYMBOLS.Open}'`,
template,
startIdx: nextOpenIdx,
endIdx: nextOpenIdx + SYMBOLS.Open.length - 1,
});
}
if (!isPresent(openIdx) && !isPresent(closeIdx)) {
parsed.push({
type: "text",
content: template.slice(position, template.length),
});
break;
}
// text before open tag
const text = template.slice(position, openIdx);
if (text.length) {
parsed.push({ type: "text", content: text });
}
const code = template.slice(openIdx + SYMBOLS.Open.length, closeIdx).trim();
if (isExpression(code)) {
parsed.push({
type: "expression",
content: stripModifierToken(code),
});
}
else {
parsed.push({ type: "statement", content: code });
}
position = closeIdx + SYMBOLS.Close.length;
}
return parsed;
}