Add ul, ol, li markup tags, restrict tags to specific positions
MODIFY
packages/@romejs/cli-reporter/Reporter.ts
packages/@romejs/cli-reporter/Reporter.ts
@@ -24,7 +24,7 @@ import {
SelectArguments,
SelectOptions,
} from './types';
-import {humanizeNumber, removeSuffix} from '@romejs/string-utils';
+import {removeSuffix} from '@romejs/string-utils';
import Progress from './Progress';
import prettyFormat from '@romejs/pretty-format';
import stream = require('stream');
@@ -42,6 +42,7 @@ type ListOptions = {
reverse?: boolean;
truncate?: number;
ordered?: boolean;
+ start?: number;
};
// rome-suppress-next-line lint/noExplicitAny
@@ -1078,64 +1079,35 @@ export default class Reporter {
return;
}
- const indent = this._getListIndentation();
-
- let tuples: Array<[number, T]>;
- if (opts.reverse === true) {
- tuples = items.reverse().map((item, i) => [items.length - i, item]);
- } else {
- tuples = items.map((item, i) => [i, item]);
- }
-
- // Truncate if necessary
let truncatedCount = 0;
- if (opts.truncate !== undefined) {
- tuples = tuples.slice(0, opts.truncate);
- truncatedCount = items.length - tuples.length;
- }
-
- let indentLength = indent.length;
- if (opts.ordered) {
- // Get the highest visible number. It could be at the start or the end depending on if it was reversed
- const highestVisible = Math.max(
- tuples[0][0],
- tuples[tuples.length - 1][0],
- );
-
- // Length of the largest visible number plus the dot for padding
- const numLen = humanizeNumber(highestVisible + 1).length + 1;
+ let start = opts.start || 0;
+ if (opts.truncate !== undefined && items.length > opts.truncate) {
+ truncatedCount = items.length - opts.truncate;
+ items = items.slice(0, opts.truncate);
+ start += truncatedCount;
+ }
- // "0. "
- indentLength += numLen + 1;
+ let buff = '';
- for (const [index, item] of tuples) {
+ for (const item of items) {
callback(
item,
(str) => {
- this.logAll(
- `${indent}<dim><pad width="${numLen}">${humanizeNumber(index + 1)}.</pad></dim> ${str}`,
- );
+ // TODO maybe set index + 1?
+ buff += `<li>${str}</li>`;
},
);
}
- } else {
- // "- "
- indentLength += 2;
- for (const [, item] of tuples) {
- callback(
- item,
- (str) => {
- this.logAll(`${indent}<dim>-</dim> ${str}`);
- },
- );
- }
+ if (opts.ordered) {
+ this.logAll(markupTag('ol', buff, {start, reversed: opts.reverse}));
+ } else {
+ this.logAll(`<ul>${buff}</ul>`);
}
if (truncatedCount > 0) {
- const indent = ' '.repeat(indentLength);
- this.logAll(`${indent}and <number>${truncatedCount}</number> others...`);
+ this.logAll(`<dim>and <number>${truncatedCount}</number> others...</dim>`);
}
}

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -35,6 +35,29 @@ export function createBlessedDiagnosticMessage(
};
}
+function join(conjunction: string, items: Array<string>): string {
+ if (items.length === 0) {
+ return '';
+ } else if (items.length === 1) {
+ return items[0];
+ } else {
+ return [...items, `${conjunction} ${items.pop()!}`].join(', ');
+ }
+}
+
+// rome-suppress-next-line lint/unusedVariables
+function andJoin(items: Array<string>): string {
+ return join('and', items);
+}
+
+function orJoin(items: Array<string>): string {
+ return join('or', items);
+}
+
+function addEmphasis(items: Array<string>): Array<string> {
+ return items.map((item) => `<emphasis>${item}</emphasis>`);
+}
+
// rome-suppress-next-line lint/AEciilnnoptxy;
type InputMessagesFactory = (...params: Array<any>) => DiagnosticMetadataString;
@@ -691,12 +714,38 @@ export const descriptions = createMessages({
},
],
}),
- INVALID_ATTRIBUTE_NAME_FOR_TAG: (tagName: string, attributeName: string) => ({
- message: markup`${attributeName} is not a valid attribute name for <${tagName}>`,
+ INVALID_ATTRIBUTE_NAME_FOR_TAG: (
+ tagName: string,
+ attributeName: string,
+ validAttributes: Array<string>,
+ ) => ({
+ message: markup`<emphasis>${attributeName}</emphasis> is not a valid attribute name for <emphasis>${tagName}</emphasis>`,
+ advice: buildSuggestionAdvice(attributeName, validAttributes),
}),
UNKNOWN_TAG_NAME: (tagName: string) => ({
message: markup`Unknown tag name <emphasis>${tagName}</emphasis>`,
}),
+ RESTRICTED_CHILD: (
+ tagName: string,
+ allowedParents: Array<string>,
+ gotParentName: string = 'none',
+ ) => ({
+ message: markup`The tag <emphasis>${tagName}</emphasis> should only appear as a child of ${orJoin(
+ addEmphasis(allowedParents),
+ )} not <emphasis>${gotParentName}</emphasis>`,
+ }),
+ RESTRICTED_PARENT: (
+ tagName: string,
+ allowedChildren: Array<string>,
+ gotChildName: string,
+ ) => ({
+ message: markup`The tag <emphasis>${tagName}</emphasis> should only contain the tags ${orJoin(
+ addEmphasis(allowedChildren),
+ )} not <emphasis>${gotChildName}</emphasis>`,
+ }),
+ RESTRICTED_PARENT_TEXT: (tagName: string) => ({
+ message: markup`The tag <emphasis>${tagName}</emphasis> should not contain any text`,
+ }),
},
// @romejs/path-match
PATH_MATCH: {

MODIFY
packages/@romejs/string-markup/Grid.ts
packages/@romejs/string-markup/Grid.ts
@@ -22,9 +22,14 @@ import {
ob1Get1,
ob1Inc,
ob1Number1,
+ ob1Sub,
} from '@romejs/ob1';
import {formatAnsi} from './ansi';
-import {humanizeFileSize, humanizeTime} from '@romejs/string-utils';
+import {
+ humanizeFileSize,
+ humanizeNumber,
+ humanizeTime,
+} from '@romejs/string-utils';
import {
formatApprox,
formatGrammarNumber,
@@ -52,6 +57,19 @@ function cursorToIndex(
type Ancestry = Array<TagNode>;
+function createTag(
+ name: TagNode['name'],
+ attributes: TagNode['attributes'],
+ children: TagNode['children'] = [],
+): TagNode {
+ return {
+ type: 'Tag',
+ name,
+ attributes,
+ children,
+ };
+}
+
export default class Grid {
constructor(opts: MarkupFormatGridOptions) {
this.viewportWidth =
@@ -223,6 +241,13 @@ export default class Grid {
this.cursor = cursor;
}
+ moveCursorBottomStart() {
+ this.moveCursor({
+ line: ob1Inc(this.getHeight()),
+ column: ob1Number1,
+ });
+ }
+
moveCursorRight(columns: Number1 = ob1Number1) {
if (this.doesOverflowViewport(this.cursor.column)) {
this.newline();
@@ -234,6 +259,12 @@ export default class Grid {
}
}
+ ensureNewline() {
+ if (this.cursor.column !== ob1Number1) {
+ this.newline();
+ }
+ }
+
newline() {
this.moveCursorDown();
this.moveCursorStart();
@@ -287,9 +318,12 @@ export default class Grid {
const word = words[i];
const isLastWord = i === words.length - 1;
- const willOverflow = this.doesOverflowViewport(
- ob1Add(this.cursor.column, word.length - 1),
- );
+ // Check if printing this word would overflow the viewport
+ // If the whole word itself wouldn't fit on it's own line then we will
+ // perform hard line wrapping in writeChar
+ const willOverflow =
+ this.doesOverflowViewport(ob1Add(this.cursor.column, word.length - 1)) &&
+ !this.doesOverflowViewport(ob1Coerce1(word.length));
if (willOverflow) {
this.newline();
}
@@ -383,6 +417,65 @@ export default class Grid {
}
}
+ drawList(tag: TagNode, ancestry: Ancestry) {
+ let items: Array<TagNode> = [];
+ for (const child of tag.children) {
+ if (child.type === 'Tag' && child.name === 'li') {
+ items.push(child);
+ }
+ }
+ if (items.length === 0) {
+ return;
+ }
+
+ this.ensureNewline();
+
+ const ordered = tag.name === 'ol';
+
+ if (ordered) {
+ const reversed = tag.attributes.reversed === 'true';
+ const startOffset: number = Number(tag.attributes.start) || 0;
+
+ if (reversed) {
+ items = items.reverse();
+ }
+
+ const highestNumSize = humanizeNumber(items.length + startOffset).length;
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ let num = startOffset;
+ if (reversed) {
+ num += items.length - i;
+ } else {
+ num += i + 1;
+ }
+
+ const humanNum = humanizeNumber(num);
+ const padding = ' '.repeat(highestNumSize - humanNum.length);
+ this.writeText(`${padding}${humanNum}. `, [createTag('dim', {})]);
+ this.drawListItem(item, ancestry);
+ }
+ } else {
+ for (const item of items) {
+ this.writeText('- ', [createTag('dim', {})]);
+ this.drawListItem(item, ancestry);
+ }
+ }
+ }
+
+ drawListItem(item: TagNode, ancestry: Ancestry) {
+ const grid = new Grid({
+ ...this.markupOptions,
+ columns: this.viewportWidth === undefined
+ ? undefined
+ : ob1Get1(ob1Sub(this.viewportWidth, this.cursor.column)),
+ });
+ grid.drawTag(item, ancestry);
+ this.drawGrid(grid);
+ this.moveCursorBottomStart();
+ }
+
drawTable(tag: TagNode, ancestry: Ancestry) {
const rows: Array<Array<TagNode>> = [];
@@ -469,10 +562,7 @@ export default class Grid {
this.moveCursorRight(ob1Inc(width));
}
- this.moveCursor({
- line: ob1Inc(this.getHeight()),
- column: ob1Number1,
- });
+ this.moveCursorBottomStart();
}
}
@@ -525,6 +615,12 @@ export default class Grid {
}
switch (tag.name) {
+ case 'ol':
+ case 'ul': {
+ this.drawList(tag, subAncestry);
+ break;
+ }
+
case 'table': {
this.drawTable(tag, subAncestry);
break;
@@ -591,32 +687,34 @@ export default class Grid {
const {emphasis, ...attributesWithoutEmphasis} = tag.attributes;
if (emphasis === 'true') {
- return this.normalizeChild({
- type: 'Tag',
- name: 'emphasis',
- attributes: {},
- children: [
+ return this.normalizeChild(
+ createTag(
+ 'emphasis',
+ {},
+ [
{
...tag,
attributes: attributesWithoutEmphasis,
},
],
- });
+ ),
+ );
}
const {dim, ...attributes} = attributesWithoutEmphasis;
if (dim === 'true') {
- return this.normalizeChild({
- type: 'Tag',
- name: 'dim',
- attributes: {},
- children: [
+ return this.normalizeChild(
+ createTag(
+ 'dim',
+ {},
+ [
{
...tag,
attributes,
},
],
- });
+ ),
+ );
}
// Insert padding
@@ -648,17 +746,16 @@ export default class Grid {
...tag,
attributes: attributesWithoutLegend,
},
- {
- type: 'Tag',
- name: 'dim',
- attributes: {},
- children: [
+ createTag(
+ 'dim',
+ {},
+ [
{
type: 'Text',
value: `[${String(index + 1)}]`,
},
],
- },
+ ),
];
}
}

MODIFY
packages/@romejs/string-markup/escape.ts
packages/@romejs/string-markup/escape.ts
@@ -66,16 +66,18 @@ export function escapeMarkup(input: string): string {
export function markupTag(
tagName: MarkupTagName,
text: string,
- attrs?: Dict<string | number | boolean>,
+ attrs?: Dict<undefined | string | number | boolean>,
): string {
let ret = `<${tagName}`;
if (attrs !== undefined) {
for (const key in attrs) {
const value = attrs[key];
+ if (value !== undefined) {
ret += markup` ${key}="${String(value)}"`;
}
}
+ }
ret += `>${text}</${tagName}>`;

MODIFY
packages/@romejs/string-markup/parse.ts
packages/@romejs/string-markup/parse.ts
@@ -27,6 +27,7 @@ import {unescapeTextValue} from './escape';
const globalAttributes: Array<string> = ['emphasis', 'dim'];
+// Tags and their corresponding supported attributes
const tags: Map<MarkupTagName, Array<string>> = new Map();
tags.set('emphasis', []);
tags.set('number', ['approx', 'pluralSuffix', 'singularSuffix']);
@@ -53,6 +54,22 @@ tags.set('td', ['align']);
tags.set('hr', []);
tags.set('pad', ['width', 'align']);
tags.set('nobr', []);
+tags.set('li', []);
+tags.set('ul', []);
+tags.set('ol', ['reversed', 'start']);
+
+// Tags that only support certain other tags as their children
+const tagsToOnlyChildren: Map<MarkupTagName, Array<MarkupTagName>> = new Map();
+tagsToOnlyChildren.set('table', ['tr']);
+tagsToOnlyChildren.set('tr', ['td']);
+tagsToOnlyChildren.set('ol', ['li']);
+tagsToOnlyChildren.set('ul', ['li']);
+
+// Tags that should only be children of other tags
+const tagsToOnlyParent: Map<MarkupTagName, Array<MarkupTagName>> = new Map();
+tagsToOnlyParent.set('tr', ['table']);
+tagsToOnlyParent.set('td', ['tr']);
+tagsToOnlyParent.set('li', ['ol', 'ul']);
//
function isStringValueChar(char: string, index: Number0, input: string): boolean {
@@ -184,7 +201,10 @@ const createStringMarkupParser = createParser((ParserCore) =>
return this.matchToken('Less') && this.lookahead().token.type === 'Slash';
}
- parseTag(headStart: Position): TagNode {
+ parseTag(
+ headStart: Position,
+ parentTagName: undefined | MarkupTagName,
+ ): TagNode {
const nameToken = this.expectToken('Word');
const tagName = (nameToken.value as MarkupTagName);
@@ -192,10 +212,46 @@ const createStringMarkupParser = createParser((ParserCore) =>
if (allowedAttributes === undefined) {
throw this.unexpected({
description: descriptions.STRING_MARKUP.UNKNOWN_TAG_NAME(tagName),
- start: this.getPositionFromIndex(nameToken.start),
+ token: nameToken,
});
}
+ // Check if this tag is restricted to certain parents
+ const onlyAllowedAsChild = tagsToOnlyParent.get(tagName);
+ if (onlyAllowedAsChild !== undefined) {
+ if (
+ parentTagName === undefined ||
+ !onlyAllowedAsChild.includes(parentTagName)
+ ) {
+ throw this.unexpected({
+ description: descriptions.STRING_MARKUP.RESTRICTED_CHILD(
+ tagName,
+ onlyAllowedAsChild,
+ parentTagName,
+ ),
+ token: nameToken,
+ });
+ }
+ }
+
+ // Check if the parent only allows certain children
+ if (parentTagName !== undefined) {
+ const onlyAllowedAsParent = tagsToOnlyChildren.get(parentTagName);
+ if (
+ onlyAllowedAsParent !== undefined &&
+ !onlyAllowedAsParent.includes(tagName)
+ ) {
+ throw this.unexpected({
+ description: descriptions.STRING_MARKUP.RESTRICTED_PARENT(
+ parentTagName,
+ onlyAllowedAsParent,
+ tagName,
+ ),
+ token: nameToken,
+ });
+ }
+ }
+
const attributes: TagAttributes = {};
const children: Children = [];
let selfClosing = false;
@@ -216,6 +272,7 @@ const createStringMarkupParser = createParser((ParserCore) =>
description: descriptions.STRING_MARKUP.INVALID_ATTRIBUTE_NAME_FOR_TAG(
tagName,
key,
+ [...allowedAttributes, ...globalAttributes],
),
});
}
@@ -262,7 +319,10 @@ const createStringMarkupParser = createParser((ParserCore) =>
!this.matchToken('EOF') &&
!this.atTagEnd()
) {
- children.push(this.parseChild());
+ const child = this.parseChild(tagName);
+ if (child !== undefined) {
+ children.push(child);
+ }
}
if (this.matchToken('EOF')) {
@@ -306,18 +366,36 @@ const createStringMarkupParser = createParser((ParserCore) =>
};
}
- parseChild(): ChildNode {
+ parseChild(parentTagName: undefined | MarkupTagName): undefined | ChildNode {
const start = this.getPosition();
const token = this.getToken();
this.nextToken();
if (token.type === 'Text') {
+ // If this tag has restricted children then no text is allowed
+ if (parentTagName !== undefined) {
+ const onlyAllowedAsParent = tagsToOnlyChildren.get(parentTagName);
+ if (onlyAllowedAsParent !== undefined) {
+ // Ignore text that's just whitespace
+ if (token.value.trim() === '') {
+ return undefined;
+ } else {
+ throw this.unexpected({
+ description: descriptions.STRING_MARKUP.RESTRICTED_PARENT_TEXT(
+ parentTagName,
+ ),
+ token,
+ });
+ }
+ }
+ }
+
return {
type: 'Text',
value: token.value,
};
} else if (token.type === 'Less') {
- return this.parseTag(start);
+ return this.parseTag(start, parentTagName);
} else {
throw this.unexpected({
description: descriptions.STRING_MARKUP.UNKNOWN_START,
@@ -328,7 +406,10 @@ const createStringMarkupParser = createParser((ParserCore) =>
parse(): Children {
const children: Children = [];
while (!this.matchToken('EOF')) {
- children.push(this.parseChild());
+ const child = this.parseChild(undefined);
+ if (child !== undefined) {
+ children.push(child);
+ }
}
return children;
}

MODIFY
packages/@romejs/string-markup/types.ts
packages/@romejs/string-markup/types.ts
@@ -63,7 +63,10 @@ export type MarkupTagName =
| 'table'
| 'tr'
| 'td'
- | 'nobr';
+ | 'nobr'
+ | 'ol'
+ | 'ul'
+ | 'li';
export type MarkupFormatFilenameNormalizer = (filename: string) => string;

Add ul, ol, li markup tags, restrict tags to specific positions