Skip to content

Commit

Permalink
fixup! fixup! feat(json): add legacy JSON generator
Browse files Browse the repository at this point in the history
  • Loading branch information
RedYetiDev committed Nov 12, 2024
1 parent e2c8fdd commit e9d7d43
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 104 deletions.
14 changes: 8 additions & 6 deletions src/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,30 @@ export const DOC_API_HEADING_TYPES = [
{
type: 'method',
regex:
/^`?(?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)*((?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\]))\([^)]*\)`?$/i,
// Group 1: foo[bar]()
// Group 2: foo.bar()
// Group 3: foobar()
/^`?(?:\w*(?:(\[[^]]+\])|(?:\.(\w+)))|(\w+))\([^)]*\)`?$/i,
},
{ type: 'event', regex: /^Event: +`?['"]?([^'"]+)['"]?`?$/i },
{
type: 'class',
regex:
/^class: +`?((?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)*[A-Z]\w+)(?: +extends +(?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)*[A-Z]\w+)?`?$/i,
/^Class: +`?([A-Z]\w+(?:\.[A-Z]\w+)*(?: +extends +[A-Z]\w+(?:\.[A-Z]\w+)*)?)`?$/i,
},
{
type: 'ctor',
regex:
/^(?:Constructor: +)?`?new +((?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)*[A-Z]\w+)\([^)]*\)`?$/i,
regex: /^(?:Constructor: +)?`?new +([A-Z]\w+(?:\.[A-Z]\w+)*)\([^)]*\)`?$/i,
},
{
type: 'classMethod',
regex:
/^Static method: +`?(?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)*((?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\]))\([^)]*\)`?$/i,
/^Static method: +`?[A-Z]\w+(?:\.[A-Z]\w+)*(?:(\[\w+\.\w+\])|\.(\w+))\([^)]*\)`?$/i,
},
{
type: 'property',
regex:
/^(?:Class property: +)?`?(?:(?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\])\.?)+((?:(?:(?:\\?_)+|\b)\w+\b|\\?\[[\w.]+\\?\]))`?$/i,
/^(?:Class property: +)?`?[A-Z]\w+(?:\.[A-Z]\w+)*(?:(\[\w+\.\w+\])|\.(\w+))`?$/i,
},
];

Expand Down
6 changes: 1 addition & 5 deletions src/generators/legacy-json-all/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ export default {
});
});

await writeFile(
join(output, 'all.json'),
JSON.stringify(generatedValue),
'utf8'
);
await writeFile(join(output, 'all.json'), JSON.stringify(generatedValue));

return generatedValue;
},
Expand Down
3 changes: 1 addition & 2 deletions src/generators/legacy-json/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ export default {
// Write it to the output file
await writeFile(
join(output, `${node.api}.json`),
JSON.stringify(section),
'utf8'
JSON.stringify(section)
);
})
);
Expand Down
52 changes: 30 additions & 22 deletions src/generators/legacy-json/utils/buildHierarchy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,50 @@
export function buildHierarchy(entries) {
const roots = [];

// Recursive helper to find the parent with a depth less than the current depth.
function findParent(entry, startIdx) {
// Base case: if we're at the beginning of the list, no valid parent exists.
if (startIdx < 0) {
throw new Error(
`Cannot find a suitable parent for entry at index ${startIdx + 1}`
);
}

const candidateParent = entries[startIdx];
const candidateDepth = candidateParent.heading.depth;

// If we find a suitable parent, return it.
if (candidateDepth < entry.heading.depth) {
candidateParent.hierarchyChildren ??= [];
return candidateParent;
}

// Recurse upwards to find a suitable parent.
return findParent(entry, startIdx - 1);
}

// Main loop to construct the hierarchy.
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const currentDepth = entry.heading.depth;

// Top-level entries are added directly to roots.
if (currentDepth <= 1) {
// We're a top-level entry
roots.push(entry);
continue;
}

// For non-root entries, find the appropriate parent.
const previousEntry = entries[i - 1];

const previousDepth = previousEntry.heading.depth;
if (currentDepth > previousDepth) {
// We're a child of the previous one
if (previousEntry.hierarchyChildren === undefined) {
previousEntry.hierarchyChildren = [];
}

if (currentDepth > previousDepth) {
previousEntry.hierarchyChildren ??= [];
previousEntry.hierarchyChildren.push(entry);
} else {
if (i < 2) {
throw new Error(`can't find parent since i < 2 (${i})`);
}

// Loop to find the entry we're a child of
for (let j = i - 2; j >= 0; j--) {
const jEntry = entries[j];
const jDepth = jEntry.heading.depth;

if (currentDepth > jDepth) {
// Found it
jEntry.hierarchyChildren.push(entry);
break;
}
}
// Use recursive helper to find the nearest valid parent.
const parent = findParent(entry, i - 2);
parent.hierarchyChildren.push(entry);
}
}

Expand Down
101 changes: 36 additions & 65 deletions src/generators/legacy-json/utils/buildSection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,15 @@ const sectionTypePlurals = {
* @returns {import('../types.d.ts').Meta | undefined}
*/
function createMeta(entry) {
const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]);
const arrify = val => (Array.isArray(val) ? val : [val]);
const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry;
if (
!added_in &&
!n_api_version &&
!deprecated_in &&
!removed_in &&
changes.length < 1
) {
return undefined;
}

return {
changes,
added: added_in ? makeArrayIfNotAlready(added_in) : undefined,
napiVersion: n_api_version
? makeArrayIfNotAlready(n_api_version)
: undefined,
deprecated: deprecated_in
? makeArrayIfNotAlready(deprecated_in)
: undefined,
removed: removed_in ? makeArrayIfNotAlready(removed_in) : undefined,
added: arrify(added_in ?? []),
napiVersion: arrify(n_api_version ?? []),
deprecated: arrify(deprecated_in ?? []),
removed: arrify(removed_in ?? []),
};
}

Expand All @@ -60,9 +48,8 @@ function createMeta(entry) {
* @returns {import('../types.d.ts').Section}
*/
function createSection(entry, head) {
const text = transformNodesToString(head.children);
return {
textRaw: text,
textRaw: transformNodesToString(head.children),
name: head.data.name,
type: head.data.type,
meta: createMeta(entry),
Expand All @@ -82,61 +69,45 @@ function parseListItem(child, entry) {
*/
const current = {};

const getRawContent = node => {
return entry.rawContent.slice(
node.position.start.offset,
node.position.end.offset
);
};

// Utility to match and extract information based on a regex pattern
const extractPattern = (text, pattern, key) => {
const [, match] = text.match(pattern) || [];
if (match) {
current[key] = match.trim().replace(/\.$/, '');
return text.replace(pattern, '');
}
return text;
};

current.textRaw = child.children
.filter(node => node.type !== 'list')
.map(node =>
entry.rawContent.slice(
node.position.start.offset,
node.position.end.offset
)
)
.map(getRawContent)
.join('')
.replace(/\s+/g, ' ')
.replaceAll(/<!--.*?-->/gs, '');

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High

This string may still contain
<!--
, which may cause an HTML element injection vulnerability.

if (!current.textRaw) {
throw new Error(`Empty list item: ${JSON.stringify(child)}`);
}

let text = current.textRaw;

// Extract name
if (RETURN_EXPRESSION.test(text)) {
current.name = 'return';
text = text.replace(RETURN_EXPRESSION, '');
} else {
const [, name] = text.match(NAME_EXPRESSION) || [];
if (name) {
current.name = name;
text = text.replace(NAME_EXPRESSION, '');
}
}

// Extract type (if provided)
const [, type] = text.match(TYPE_EXPRESSION) || [];
if (type) {
current.type = type;
text = text.replace(TYPE_EXPRESSION, '');
}
// Extract `name`, `type`, `default`, and `desc` fields with helper functions
text = extractPattern(text, RETURN_EXPRESSION, 'name') || text;
text = current.name ? text : extractPattern(text, NAME_EXPRESSION, 'name');
text = extractPattern(text, TYPE_EXPRESSION, 'type');
text = extractPattern(text, DEFAULT_EXPRESSION, 'default');

// Remove leading hyphens
text = text.replace(LEADING_HYPHEN, '');

// Extract default value (if exists)
const [, defaultValue] = text.match(DEFAULT_EXPRESSION) || [];
if (defaultValue) {
current.default = defaultValue.replace(/\.$/, '');
text = text.replace(DEFAULT_EXPRESSION, '');
}

// Add remaining text to the desc
if (text) {
current.desc = text;
}
// Remove leading hyphens and remaining text becomes `desc`, if any.
current.desc = text.replace(LEADING_HYPHEN, '').trim() || undefined;

const options = child.children.find(child => child.type === 'list');
if (options) {
current.options = options.children.map(child =>
// Recursively parse options if available
const optionsNode = child.children.find(child => child.type === 'list');
if (optionsNode) {
current.options = optionsNode.children.map(child =>
parseListItem(child, entry)
);
}
Expand Down
15 changes: 11 additions & 4 deletions src/utils/parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,19 @@ export const parseHeadingIntoMetadata = (heading, depth) => {
// Attempts to get a match from one of the heading types, if a match is found
// we use that type as the heading type, and extract the regex expression match group
// which should be the inner "plain" heading content (or the title of the heading for navigation)
const [, innerHeading] = heading.match(regex) ?? [];

if (innerHeading && innerHeading.length) {
return { text: heading, type, name: innerHeading, depth };
const [, ...matches] = heading.match(regex) ?? [];

if (matches?.length) {
return {
text: heading,
type,
name: matches.filter(Boolean).at(-1),
depth,
};
}
}

console.log(undefined, heading);

return { text: heading, type: 'module', name: heading, depth };
};

0 comments on commit e9d7d43

Please sign in to comment.