Skip to content

Commit

Permalink
optim jsdoc and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNorthMemory committed Aug 13, 2024
1 parent d27e531 commit 3c0c5b3
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 11 deletions.
75 changes: 70 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,75 @@

const patternsConfig = require('./patterns.js');

/**
* @typedef {import('markdown-it')} MarkdownIt
*
* @typedef {import('markdown-it/lib/rules_core/state_core.mjs').default} StateCore
*
* @typedef {import('markdown-it/lib/token.mjs').default} Token
*
* @typedef {import('markdown-it/lib/token.mjs').Nesting} Nesting
*
* @typedef {Object} Options
* @property {!string} leftDelimiter left delimiter, default is `{`(left basket)
* @property {!string} rightDelimiter right delimiter, default is `}`(right basket)
* @property {AllowedAttribute[]} allowedAttributes empty means no limit
*
* @typedef {string|RegExp} AllowedAttribute rule of allowed attribute
*
* @typedef {[string, string]} AttributePair
*
* @typedef {[number, number]} SourceLineInfo
*
* @typedef {Object} CurlyAttrsPattern
* @property {string} name
* @property {DetectingRule[]} tests
* @property {(tokens: Token[], i: number, j?: number) => void} transform
*
* @typedef {Object} MatchedResult
* @property {boolean} match true means matched
* @property {number?} j postion index number of Array<{@link Token}>
*
* @typedef {(str: string) => boolean} DetectingStrRule
*
* @typedef {Object} DetectingRule rule for testing {@link Token}'s properties
* @property {number=} shift offset index number of Array<{@link Token}>
* @property {number=} position fixed index number of Array<{@link Token}>
* @property {(string | DetectingStrRule)=} type
* @property {(string | DetectingStrRule)=} tag
* @property {DetectingRule[]=} children
* @property {(string | DetectingStrRule)=} content
* @property {(string | DetectingStrRule)=} markup
* @property {(string | DetectingStrRule)=} info
* @property {Nesting=} nesting
* @property {number=} level
* @property {boolean=} block
* @property {boolean=} hidden
* @property {AttributePair[]=} attrs
* @property {SourceLineInfo[]=} map
* @property {any=} meta
*/

/** @type {Options} */
const defaultOptions = {
leftDelimiter: '{',
rightDelimiter: '}',
allowedAttributes: []
};

/**
* @param {MarkdownIt} md
* @param {Options=} options_
*/
module.exports = function attributes(md, options_) {
let options = Object.assign({}, defaultOptions);
options = Object.assign(options, options_);

const patterns = patternsConfig(options);

/**
* @param {StateCore} state
*/
function curlyAttrs(state) {
const tokens = state.tokens;

Expand Down Expand Up @@ -43,12 +100,13 @@ module.exports = function attributes(md, options_) {
/**
* Test if t matches token stream.
*
* @param {array} tokens
* @param {Token[]} tokens
* @param {number} i
* @param {object} t Test to match.
* @return {object} { match: true|false, j: null|number }
* @param {DetectingRule} t
* @returns {MatchedResult}
*/
function test(tokens, i, t) {
/** @type {MatchedResult} */
const res = {
match: false,
j: null // position of child
Expand Down Expand Up @@ -78,7 +136,9 @@ function test(tokens, i, t) {
return res;
}
let match;
/** @type {DetectingRule[]} */
const childTests = t.children;
/** @type {Token[]} */
const children = token.children;
if (childTests.every(tt => tt.position !== undefined)) {
// positions instead of shifts, do not loop all children
Expand Down Expand Up @@ -141,14 +201,19 @@ function isArrayOfFunctions(arr) {
/**
* Get n item of array. Supports negative n, where -1 is last
* element in array.
* @param {array} arr
* @param {Token[]} arr
* @param {number} n
* @returns {Token=}
*/
function get(arr, n) {
return n >= 0 ? arr[n] : arr[arr.length + n];
}

// get last element of array, safe - returns {} if not found
/**
* get last element of array, safe - returns {} if not found
* @param {DetectingRule[]} arr
* @returns {DetectingRule}
*/
function last(arr) {
return arr.slice(-1)[0] || {};
}
22 changes: 22 additions & 0 deletions patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

const utils = require('./utils.js');

/**
* @param {import('.').Options} options
* @returns {import('.').CurlyAttrsPattern[]}
*/
module.exports = options => {
const __hr = new RegExp('^ {0,3}[-*_]{3,} ?'
+ utils.escapeRegExp(options.leftDelimiter)
Expand Down Expand Up @@ -58,6 +62,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const endChar = token.content.indexOf(options.rightDelimiter);
Expand Down Expand Up @@ -124,6 +131,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const content = token.content;
Expand Down Expand Up @@ -157,6 +167,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const content = token.content;
Expand Down Expand Up @@ -227,6 +240,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const content = token.content;
Expand Down Expand Up @@ -258,6 +274,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const attrs = utils.getAttrs(token.content, 0, options);
Expand Down Expand Up @@ -319,6 +338,9 @@ module.exports = options => {
]
}
],
/**
* @param {!number} j
*/
transform: (tokens, i, j) => {
const token = tokens[i].children[j];
const content = token.content;
Expand Down
29 changes: 29 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ function describeTestsWithOptions(options, postText) {
const res = utils.getAttrs(replaceDelimiters(src, options), 0, options);
assert.deepEqual(res, expected);
});

it(replaceDelimiters('should parse attributes whose are ignored the key chars(\\t,\\n,\\f,\\s,/,>,",\',=) eg: {gt>=true slash/=trace i\\td "q\\fnu e\'r\\ny"=}', options), () => {
const src = '{gt>=true slash/=trace i\td "q\fu\ne\'r\ny"=}';
const expected = [['gt', 'true'], ['slash', 'trace'], ['id', ''], ['query', '']];
const res = utils.getAttrs(replaceDelimiters(src, options), 0, options);
assert.deepEqual(res, expected);
});

it(replaceDelimiters('should throw an error while calling `hasDelimiters` with an invalid `where` param', options), () => {
assert.throws(() => utils.hasDelimiters(0, options), { name: 'Error', message: /Should be "start", "end" or "only"/ });
assert.throws(() => utils.hasDelimiters('', options), { name: 'Error', message: /Should be "start", "end" or "only"/ });
assert.throws(() => utils.hasDelimiters(null, options), { name: 'Error', message: /Should be "start", "end" or "only"/ });
assert.throws(() => utils.hasDelimiters(undefined, options), { name: 'Error', message: /Should be "start", "end" or "only"/ });
assert.throws(() => utils.hasDelimiters('center', options)('has {#test} delimiters'), { name: 'Error', message: /expected 'start', 'end' or 'only'/ });
});

it('should escape html entities(&,<,>,") eg: <a href="?a&b">TOC</a>', () => {
const src = '<a href="a&b">TOC</a>';
const expected = '&lt;a href=&quot;a&amp;b&quot;&gt;TOC&lt;/a&gt;';
const res = utils.escapeHtml(src);
assert.deepEqual(res, expected);
});

it('should keep the origional input which is not contains(&,<,>,") char(s) eg: |a|b|', () => {
const src = '|a|b|';
const expected = '|a|b|';
const res = utils.escapeHtml(src);
assert.deepEqual(res, expected);
});
});

describe('markdown-it-attrs' + postText, () => {
Expand Down
39 changes: 33 additions & 6 deletions utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/**
* @typedef {import('.').Token} Token
* @typedef {import('.').Options} Options
* @typedef {import('.').AttributePair} AttributePair
* @typedef {import('.').AllowedAttribute} AllowedAttribute
* @typedef {import('.').DetectingStrRule} DetectingStrRule
*/
/**
* parse {.class #id key=val} strings
* @param {string} str: string to parse
* @param {int} start: where to start parsing (including {)
* @returns {2d array}: [['key', 'val'], ['class', 'red']]
* @param {number} start: where to start parsing (including {)
* @param {Options} options
* @returns {AttributePair[]}: [['key', 'val'], ['class', 'red']]
*/
exports.getAttrs = function (str, start, options) {
// not tab, line feed, form feed, space, solidus, greater than sign, quotation mark, apostrophe and equals sign
Expand Down Expand Up @@ -95,6 +103,9 @@ exports.getAttrs = function (str, start, options) {
return attrs.filter(function (attrPair) {
const attr = attrPair[0];

/**
* @param {AllowedAttribute} allowedAttribute
*/
function isAllowedAttribute (allowedAttribute) {
return (attr === allowedAttribute
|| (allowedAttribute instanceof RegExp && allowedAttribute.test(attr))
Expand All @@ -111,8 +122,8 @@ exports.getAttrs = function (str, start, options) {

/**
* add attributes from [['key', 'val']] list
* @param {array} attrs: [['key', 'val']]
* @param {token} token: which token to add attributes
* @param {AttributePair[]} attrs: [['key', 'val']]
* @param {Token} token: which token to add attributes
* @returns token
*/
exports.addAttrs = function (attrs, token) {
Expand All @@ -136,8 +147,9 @@ exports.addAttrs = function (attrs, token) {
* end: 'asdf {.a}'
* only: '{.a}'
*
* @param {string} where to expect {} curly. start, end or only.
* @return {function(string)} Function which testes if string has curly.
* @param {'start'|'end'|'only'} where to expect {} curly. start, end or only.
* @param {Options} options
* @return {DetectingStrRule} Function which testes if string has curly.
*/
exports.hasDelimiters = function (where, options) {

Expand All @@ -156,6 +168,9 @@ exports.hasDelimiters = function (where, options) {
return false;
}

/**
* @param {string} curly
*/
function validCurlyLength (curly) {
const isClass = curly.charAt(options.leftDelimiter.length) === '.';
const isId = curly.charAt(options.leftDelimiter.length) === '#';
Expand Down Expand Up @@ -204,6 +219,8 @@ exports.hasDelimiters = function (where, options) {

/**
* Removes last curly from string.
* @param {string} str
* @param {Options} options
*/
exports.removeDelimiter = function (str, options) {
const start = escapeRegExp(options.leftDelimiter);
Expand Down Expand Up @@ -231,6 +248,8 @@ exports.escapeRegExp = escapeRegExp;

/**
* find corresponding opening block
* @param {Token[]} tokens
* @param {number} i
*/
exports.getMatchingOpeningToken = function (tokens, i) {
if (tokens[i].type === 'softbreak') {
Expand Down Expand Up @@ -266,10 +285,18 @@ const HTML_REPLACEMENTS = {
'"': '&quot;'
};

/**
* @param {string} ch
* @returns {string}
*/
function replaceUnsafeChar(ch) {
return HTML_REPLACEMENTS[ch];
}

/**
* @param {string} str
* @returns {string}
*/
exports.escapeHtml = function (str) {
if (HTML_ESCAPE_TEST_RE.test(str)) {
return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar);
Expand Down

0 comments on commit 3c0c5b3

Please sign in to comment.