Skip to content

Commit

Permalink
feat: refactor custom attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Jul 19, 2024
1 parent a23f694 commit 971d02a
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 157 deletions.
199 changes: 199 additions & 0 deletions src/transform/plugins/table/attrs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
type Attrs = 'class' | 'id' | 'attr';

export class AttrsParser {
DELIMITER = '=';
SEPARATOR = ' ';
QUOTATION = '"';
/* allowed in keys / values chars */
ALLOWED_CHARS = /[a-zA-Z0-9_\- {}.|/]/;
/* allowed in all query chars */
VALIDATION_CHARS = /[a-zA-Z0-9_\- {}.#="|/]/;

#key = '';
#pending = '';
#isInsideQuotation = false;
#didQuotationClosed = false;
#currentKeyType: Attrs | undefined;

#selectors: Record<Attrs, RegExp> = {
id: /#/,
class: /\./,
attr: /[a-zA-Z-_]/,
};

#handlers = Object.entries(this.#selectors) as [Attrs, RegExp][];
#state: Record<string, string[]> = {};

parse(target: string): Record<string, string[]> {
/* escape from {} */
const content = this.extract(target);

if (!content) {
return {};
}

for (const char of content) {
this.next(char);
}

/* end-of-content mark */
this.next(this.SEPARATOR);

this.clear();

return this.#state;
}

private extract(target: string): string | false {
if (!target.startsWith('{')) {
return false;
}
let balance = 1;

for (let i = 1; i < target.length; i++) {
const char = target[i];

if (char === '}') {
balance--;
}

if (char === '{') {
balance++;
}

if (balance === 0) {
const contentInside = target.slice(1, i).trim();

return contentInside;
}

if (balance < 0) {
return false;
}

if (!this.VALIDATION_CHARS.test(char)) {
return false;
}
}

return false;
}

private next(value: string) {
if (!this.#currentKeyType) {
this.#currentKeyType = this.type(value);

if (this.#currentKeyType === 'attr') {
this.#pending = value;
}

return;
}

if (this.isSeparator(value)) {
if (!this.#pending) {
/* (name= ) construction */
if (!this.#isInsideQuotation) {
this.append(this.#key, ' ');
this.clear();

return;
}
}

/* single key (.name #id contenteditable) */
if (!this.#key && this.#pending) {
this.append();
this.clear();

return;
}

/* trying to find close quotation */
if (this.#isInsideQuotation && !this.#didQuotationClosed) {
this.#pending += value;
return;
}

if (this.#isInsideQuotation && this.#didQuotationClosed) {
this.append(this.#key, this.#pending);
}

if (!this.#isInsideQuotation && !this.#didQuotationClosed) {
this.append(this.#key, this.#pending);
}

this.clear();

return;
}

if (this.isAllowedChar(value)) {
this.#pending += value;

return;
}

if (this.isQuotation(value)) {
if (this.#isInsideQuotation) {
this.#didQuotationClosed = true;
} else {
this.#isInsideQuotation = true;
}
}

if (this.isDelimiter(value)) {
/* symbol is not delimiter, adding it to value */
if (this.#key) {
this.#pending += value;

return;
}

this.#key = this.#pending;
this.#pending = '';
}
}

private type(of: string): Attrs | undefined {
return this.#handlers.find(([_, regex]) => regex.test(of))?.[0];
}

private append(key: string | undefined = this.#currentKeyType, value: string = this.#pending) {
if (!key) {
return;
}

if (!this.#state[key]) {
this.#state[key] = [];
}

this.#state[key].push(value);
}

private clear() {
this.#key = '';
this.#pending = '';

this.#isInsideQuotation = false;
this.#didQuotationClosed = false;

this.#currentKeyType = undefined;
}

private isDelimiter(target: string) {
return target === this.DELIMITER;
}

private isSeparator(target: string) {
return target === this.SEPARATOR;
}

private isQuotation(target: string) {
return target === this.QUOTATION;
}

private isAllowedChar(target: string) {
return this.ALLOWED_CHARS.test(target);
}
}
18 changes: 14 additions & 4 deletions src/transform/plugins/table/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import StateBlock from 'markdown-it/lib/rules_block/state_block';
import {MarkdownItPluginCb} from '../typings';
import Token from 'markdown-it/lib/token';
import {parseAttrs} from './utils';
import {AttrsParser} from './attrs';

const pluginName = 'yfm_table';
const pipeChar = 0x7c; // |
Expand Down Expand Up @@ -225,7 +225,9 @@ function extractAttributes(state: StateBlock, pos: number): Record<string, strin
const attrsStringStart = state.skipSpaces(pos);
const attrsString = state.src.slice(attrsStringStart);

return parseAttrs(attrsString) ?? {};
const attrsParser = new AttrsParser();

return attrsParser.parse(attrsString);
}

/**
Expand All @@ -243,7 +245,10 @@ function extractAndApplyClassFromToken(contentToken: Token, tdOpenToken: Token):
if (!allAttrs) {
return;
}
const attrsClass = parseAttrs(allAttrs[0].trim())?.class.join(' ');

const attrs = new AttrsParser().parse(allAttrs[0].trim());
const attrsClass = attrs?.class?.join(' ');

if (attrsClass) {
tdOpenToken.attrSet('class', attrsClass);
// remove the class from the token so that it's not propagated to tr or table level
Expand Down Expand Up @@ -399,10 +404,15 @@ const yfmTable: MarkdownItPluginCb = (md) => {
const tableStart = state.tokens.length;
token = state.push('yfm_table_open', 'table', 1);

for (const [property, values] of Object.entries(attrs)) {
const {attr: singleKeyAttrs = [], ...fullAttrs} = attrs;
for (const [property, values] of Object.entries(fullAttrs)) {
token.attrJoin(property, values.join(' '));
}

for (const attr of singleKeyAttrs) {
token.attrJoin(attr, 'true');
}

token.map = [startLine, endOfTable];

token = state.push('yfm_tbody_open', 'tbody', 1);
Expand Down
107 changes: 0 additions & 107 deletions src/transform/plugins/table/utils.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/transform/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ const htmlAttrs = [
'referrerpolicy',
'aria-describedby',
'data-*',
'wide-content',
];

const svgAttrs = [
Expand Down
Loading

0 comments on commit 971d02a

Please sign in to comment.