import type { AttributeToken, TagToken, Token } from 'pug-lexer'; import type { Logger } from '../logger'; /** * Returns the previous tag token if there was one. * * @param tokens The token array. * @param index The current index within the token array.. * @returns Previous tag token if there was one. */ export function previousTagToken( tokens: ReadonlyArray, index: number, ): TagToken | undefined { for (let i: number = index - 1; i >= 0; i--) { const token: Token | undefined = tokens[i]; if (!token) { return; } if (token.type === 'tag') { return token; } } return; } /** * Returns the previous attribute token between the current token and the last occurrence of a `start-attributes` token. * * @param tokens A reference to the whole token array. * @param index The current index on which the cursor is in the token array. * @returns Previous attribute token if there was one. */ export function previousNormalAttributeToken( tokens: ReadonlyArray, index: number, ): AttributeToken | undefined { for (let i: number = index - 1; i > 0; i--) { const token: Token | undefined = tokens[i]; if (!token || token.type === 'start-attributes') { return; } if (token.type === 'attribute') { if (token.name !== 'class' && token.name !== 'id') { return token; } } } return; } /** * Returns the previous type attribute token or undefined if no attribute is present. * * @param tokens A reference to the whole token array. * @param index The current index on which the cursor is in the token array. * @returns Previous attribute token if there was one. */ export function previousTypeAttributeToken( tokens: ReadonlyArray, index: number, ): AttributeToken | undefined { for (let i: number = index - 1; i > 0; i--) { const token: Token | undefined = tokens[i]; if (!token || token.type === 'start-attributes' || token.type === 'tag') { return; } if (token.type === 'attribute') { if (token.name === 'type') { return token; } } } return; } /** * Unwraps line feeds from a given value. * * @param value The value to unwrap. * @returns The unwrapped result. */ export function unwrapLineFeeds(value: string): string { return value.includes('\n') ? value .split('\n') .map((part) => part.trim()) .map((part) => (part[0] === '.' ? '' : ' ') + part) .join('') .trim() : value; } /** * Indicates whether the attribute is a `style` normal attribute. * * --- * * Example style tag: * ``` * span(style="color: red") * ``` * * In this case `name` is `style` and `val` is `"color: red"`. * * --- * * @param name Name of tag attribute. * @param val Value of `style` tag attribute. * @returns Whether it's a style attribute that is quoted or not. */ export function isStyleAttribute(name: string, val: string): boolean { return name === 'style' && isQuoted(val); } /** * Indicates whether the value is surrounded by the `start` and `end` parameters. * * @param val Value of a tag attribute. * @param start The left hand side of the wrapping. * @param end The right hand side of the wrapping. * @param offset The offset from left and right where to search from. * @returns Whether the value is wrapped with start and end from the offset or not. */ export function isWrappedWith( val: string, start: string, end: string, offset: number = 0, ): boolean { return ( val.startsWith(start, offset) && val.endsWith(end, val.length - offset) ); } /** * Indicates whether the value is surrounded by quotes. * * --- * * Example with double quotes: * ``` * a(href="#") * ``` * * In this case `val` is `"#"`. * * --- * * Example with single quotes: * ``` * a(href='#') * ``` * * In this case `val` is `'#'`. * * --- * * Example with no quotes: * ``` * - const route = '#'; * a(href=route) * ``` * * In this case `val` is `route`. * * --- * * Special cases: * ``` * a(href='/' + '#') * a(href="/" + "#") * ``` * * These cases should not be treated as quoted. * * --- * * @param val Value of tag attribute. * @returns Whether the value is quoted or not. */ export function isQuoted(val: string): boolean { if (/^(["'`])(.*)\1$/.test(val)) { // Regex for checking if there are any unescaped quotations. const regex: RegExp = new RegExp(`${val[0]}(? { if (escaped === otherQuote) { return escaped; } if (quote === enclosingQuote) { return `\\${quote}`; } if (quote) { return quote; } return unescapeUnnecessaryEscapes && /^[^\\nrvtbfux\r\n\u2028\u2029"'0-7]$/.test(escaped) ? escaped : `\\${escaped}`; }, ); return enclosingQuote + newContent + enclosingQuote; } /** * See [issue #9](https://github.com/prettier/plugin-pug/issues/9) for more details. * * @param code Code that is checked. * @param quotes Quotes. * @param otherQuotes Opposite of quotes. * @param logger A logger. * @returns Whether dangerous quote combinations where detected or not. */ export function detectDangerousQuoteCombination( code: string, quotes: "'" | '"', otherQuotes: "'" | '"', logger: Logger, ): boolean { // Index of primary quote const q1: number = code.indexOf(quotes); // Index of secondary (other) quote const q2: number = code.indexOf(otherQuotes); // Index of backtick const qb: number = code.indexOf('`'); if (q1 >= 0 && q2 >= 0 && q2 > q1 && (qb < 0 || q1 < qb)) { logger.log({ code, quotes, otherQuotes, q1, q2, qb }); return true; } return false; }