Skip to content

Commit

Permalink
Refactor core package to use Node types (MarkBind#2221)
Browse files Browse the repository at this point in the history
Refactor core package to use Node types

In MarkBind#2201, we introduced a unifying type for nodes. Most of the core
package is not yet using the new types.

Let's refactor the core package to utilise these new types, and add a
utility parseHTML method that avoids further `as unknown as` typecasts
throughout the codebase. Doing so allows us to make full use of
TypeScript's type-checking abilities while avoiding code duplication.

For more information on the rationale of the unifying types, refer to
MarkBind#2201.
  • Loading branch information
jovyntls authored and yucheng11122017 committed Mar 22, 2023
1 parent 08e0bac commit 5edcc2f
Show file tree
Hide file tree
Showing 17 changed files with 163 additions and 185 deletions.
16 changes: 8 additions & 8 deletions packages/core/src/html/FootnoteProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cheerio from 'cheerio';
import { DomElement } from 'htmlparser2';
import { MbNode, parseHTML } from '../utils/node';
import { MARKBIND_FOOTNOTE_POPOVER_ID_PREFIX } from './constants';

/*
Expand All @@ -13,7 +13,7 @@ export class FootnoteProcessor {
this.renderedFootnotes = [];
}

processMbTempFootnotes(node: DomElement) {
processMbTempFootnotes(node: MbNode) {
const $ = cheerio(node);
const content = $.html();
if (content) {
Expand All @@ -22,25 +22,25 @@ export class FootnoteProcessor {
$.remove();
}

combineFootnotes(processNode: (nd: DomElement) => any): string {
combineFootnotes(processNode: (nd: MbNode) => any): string {
let hasFootnote = false;
const prefix = '<hr class="footnotes-sep">\n<section class="footnotes">\n<ol class="footnotes-list">\n';

const footnotesWithPopovers = this.renderedFootnotes.map((footNoteBlock) => {
const $ = cheerio.load(footNoteBlock);
let popoversHtml = '';

$('li.footnote-item').each((index, li) => {
$('li.footnote-item').each((_index, li) => {
hasFootnote = true;
const popoverId = `${MARKBIND_FOOTNOTE_POPOVER_ID_PREFIX}${(li as any).attribs.id}`;
const popoverNode = cheerio.parseHTML(`<popover id="${popoverId}">
const popoverNode = parseHTML(`<popover id="${popoverId}">
<div #content>
${$(li).html()}
</div>
</popover>`)[0];
processNode(popoverNode as unknown as DomElement);
</popover>`)[0] as MbNode;
processNode(popoverNode);

popoversHtml += cheerio.html(popoverNode as any);
popoversHtml += cheerio.html(popoverNode);
});

return `${popoversHtml}\n${footNoteBlock}\n`;
Expand Down
37 changes: 16 additions & 21 deletions packages/core/src/html/MdAttributeRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import cheerio from 'cheerio';
import has from 'lodash/has';
import { DomElement } from 'htmlparser2';
import { getVslotShorthandName } from './vueSlotSyntaxProcessor';
import type { MarkdownProcessor } from './MarkdownProcessor';
import * as logger from '../utils/logger';
import { createSlotTemplateNode } from './elements';
import { NodeOrText } from '../utils/node';
import { MbNode, NodeOrText, parseHTML } from '../utils/node';

const _ = {
has,
Expand All @@ -29,11 +27,8 @@ export class MdAttributeRenderer {
* @param isInline Whether to process the attribute with only inline markdown-it rules
* @param slotName Name attribute of the <slot> element to insert, which defaults to the attribute name
*/
processAttributeWithoutOverride(node: DomElement, attribute: string,
processAttributeWithoutOverride(node: MbNode, attribute: string,
isInline: boolean, slotName = attribute): void {
if (!node.attribs) {
return;
}
const hasAttributeSlot = node.children
&& node.children.some(child => getVslotShorthandName(child) === slotName);

Expand Down Expand Up @@ -63,7 +58,7 @@ export class MdAttributeRenderer {
* @returns {boolean} whether the node has both the slot and attribute
*/
// eslint-disable-next-line class-methods-use-this
hasSlotOverridingAttribute(node: DomElement, attribute: string, slotName = attribute): boolean {
hasSlotOverridingAttribute(node: MbNode, attribute: string, slotName = attribute): boolean {
const hasNamedSlot = node.children
&& node.children.some(child => getVslotShorthandName(child) === slotName);
if (!hasNamedSlot || !node.attribs) {
Expand All @@ -80,7 +75,7 @@ export class MdAttributeRenderer {
return hasAttribute;
}

processPopoverAttributes(node: DomElement) {
processPopoverAttributes(node: MbNode) {
if (!this.hasSlotOverridingAttribute(node, 'header')) {
this.processAttributeWithoutOverride(node, 'header', true);
}
Expand All @@ -103,11 +98,11 @@ export class MdAttributeRenderer {
this.processAttributeWithoutOverride(node, 'content', true);
}

processTooltip(node: DomElement) {
processTooltip(node: MbNode) {
this.processAttributeWithoutOverride(node, 'content', true);
}

processModalAttributes(node: DomElement) {
processModalAttributes(node: MbNode) {
if (!this.hasSlotOverridingAttribute(node, 'header')) {
this.processAttributeWithoutOverride(node, 'header', true);
}
Expand All @@ -117,7 +112,7 @@ export class MdAttributeRenderer {
* Panels
*/

processPanelAttributes(node: DomElement) {
processPanelAttributes(node: MbNode) {
this.processAttributeWithoutOverride(node, 'alt', false, '_alt');
if (!this.hasSlotOverridingAttribute(node, 'header')) {
this.processAttributeWithoutOverride(node, 'header', false);
Expand All @@ -128,33 +123,33 @@ export class MdAttributeRenderer {
* Questions, QOption, and Quizzes
*/

processQuestion(node: DomElement) {
processQuestion(node: MbNode) {
this.processAttributeWithoutOverride(node, 'header', false);
this.processAttributeWithoutOverride(node, 'hint', false);
this.processAttributeWithoutOverride(node, 'answer', false);
}

processQOption(node: DomElement) {
processQOption(node: MbNode) {
this.processAttributeWithoutOverride(node, 'reason', false);
}

processQuiz(node: DomElement) {
processQuiz(node: MbNode) {
this.processAttributeWithoutOverride(node, 'intro', false);
}

/*
* Tabs
*/

processTabAttributes(node: DomElement) {
processTabAttributes(node: MbNode) {
this.processAttributeWithoutOverride(node, 'header', true);
}

/*
* Boxes
*/

processBoxAttributes(node: DomElement) {
processBoxAttributes(node: MbNode) {
this.processAttributeWithoutOverride(node, 'icon', true);
this.processAttributeWithoutOverride(node, 'header', false);
}
Expand All @@ -163,7 +158,7 @@ export class MdAttributeRenderer {
* Dropdowns
*/

processDropdownAttributes(node: DomElement) {
processDropdownAttributes(node: MbNode) {
if (!this.hasSlotOverridingAttribute(node, 'header')) {
this.processAttributeWithoutOverride(node, 'header', true);
}
Expand All @@ -173,7 +168,7 @@ export class MdAttributeRenderer {
* Thumbnails
*/

processThumbnailAttributes(node: DomElement) {
processThumbnailAttributes(node: MbNode) {
if (!node.attribs) {
return;
}
Expand All @@ -189,11 +184,11 @@ export class MdAttributeRenderer {
}

const renderedText = this.markdownProcessor.renderMdInline(text);
node.children = cheerio.parseHTML(renderedText) as unknown as DomElement[];
node.children = parseHTML(renderedText);
delete node.attribs.text;
}

processScrollTopButtonAttributes(node: DomElement) {
processScrollTopButtonAttributes(node: MbNode) {
this.processAttributeWithoutOverride(node, 'icon', true);
}
}
22 changes: 10 additions & 12 deletions packages/core/src/html/NodeProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class NodeProcessor {
/*
* Frontmatter collection
*/
_processFrontmatter(node: MbNode, context: Context) {
private _processFrontmatter(node: MbNode, context: Context) {
let currentFrontmatter = {};
const frontmatter = cheerio(node);
if (!context.processingOptions.omitFrontmatter && frontmatter.text().trim()) {
Expand Down Expand Up @@ -153,18 +153,16 @@ export class NodeProcessor {
* Removes the node if modal id already exists, processes node otherwise
*/
private processModal(node: MbNode) {
if (node.attribs) {
if (this.processedModals[node.attribs.id]) {
cheerio(node).remove();
} else {
this.processedModals[node.attribs.id] = true;
if (this.processedModals[node.attribs.id]) {
cheerio(node).remove();
} else {
this.processedModals[node.attribs.id] = true;

// Transform deprecated slot names; remove when deprecating
renameSlot(node, 'modal-header', 'header');
renameSlot(node, 'modal-footer', 'footer');
// Transform deprecated slot names; remove when deprecating
renameSlot(node, 'modal-header', 'header');
renameSlot(node, 'modal-footer', 'footer');

this.mdAttributeRenderer.processModalAttributes(node);
}
this.mdAttributeRenderer.processModalAttributes(node);
}
}

Expand Down Expand Up @@ -397,7 +395,7 @@ export class NodeProcessor {
return;
}
const mainHtmlNodes = dom.map((d) => {
let processed;
let processed: NodeOrText;
try {
processed = this.traverse(d, context);
} catch (err: any) {
Expand Down
14 changes: 6 additions & 8 deletions packages/core/src/html/SiteLinkManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import has from 'lodash/has';
import { DomElement } from 'htmlparser2';
import * as linkProcessor from './linkProcessor';
import type { NodeProcessorConfig } from './NodeProcessor';
import { MbNode } from '../utils/node';

const _ = { has };

Expand Down Expand Up @@ -51,16 +51,14 @@ export class SiteLinkManager {
* Add a link to the intralinkCollection to be validated later,
* if the node should be validated and intralink validation is not disabled.
*/
collectIntraLinkToValidate(node: DomElement, cwf: string) {
if (node.name && !tagsToValidate.has(node.name)) {
collectIntraLinkToValidate(node: MbNode, cwf: string) {
if (!tagsToValidate.has(node.name)) {
return 'Should not validate';
}

if (node.attribs) {
const hasIntralinkValidationDisabled = _.has(node.attribs, 'no-validation');
if (hasIntralinkValidationDisabled) {
return 'Intralink validation disabled';
}
const hasIntralinkValidationDisabled = _.has(node.attribs, 'no-validation');
if (hasIntralinkValidationDisabled) {
return 'Intralink validation disabled';
}

const resourcePath = linkProcessor.getDefaultTagsResourcePath(node);
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/html/codeblockProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cheerio from 'cheerio';
import { DomElement } from 'htmlparser2';
import has from 'lodash/has';
import { NodeOrText, MbNode } from '../utils/node';

const md = require('../lib/markdown-it');
const util = require('../lib/markdown-it/utils');
Expand All @@ -20,9 +20,9 @@ interface TraverseLinePartData {
* @param node The node of the line part to be traversed
* @param hlStart The highlight start position, relative to the start of the line part
* @param hlEnd The highlight end position, relative to the start of the line part
* @returns {object} An object that contains data to be used by the node's parent.
* @returns An object that contains data to be used by the node's parent.
*/
function traverseLinePart(node: DomElement, hlStart: number, hlEnd: number): TraverseLinePartData {
function traverseLinePart(node: NodeOrText, hlStart: number, hlEnd: number): TraverseLinePartData {
const resData: TraverseLinePartData = {
numCharsTraversed: 0,
shouldParentHighlight: false,
Expand Down Expand Up @@ -128,7 +128,7 @@ function traverseLinePart(node: DomElement, hlStart: number, hlEnd: number): Tra
* traverses over the line and applies the highlight.
* @param node Root of the code block element, which is the 'pre' node
*/
export function highlightCodeBlock(node: DomElement) {
export function highlightCodeBlock(node: MbNode) {
if (!node.children) {
return;
}
Expand All @@ -155,8 +155,8 @@ export function highlightCodeBlock(node: DomElement) {
* @param node the code block element, which is the 'code' node
* @param showCodeLineNumbers true if line numbers should be shown, false otherwise
*/
export function setCodeLineNumbers(node: DomElement, showCodeLineNumbers: boolean) {
const existingClass = node.attribs?.class || '';
export function setCodeLineNumbers(node: MbNode, showCodeLineNumbers: boolean) {
const existingClass = node.attribs.class || '';
const styleClassRegex = /(^|\s)(no-)?line-numbers($|\s)/;
const hasStyleClass = styleClassRegex.test(existingClass);
if (hasStyleClass) {
Expand Down
14 changes: 6 additions & 8 deletions packages/core/src/html/headerProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import cheerio from 'cheerio';
import slugify from '@sindresorhus/slugify';
import has from 'lodash/has';
import { DomElement } from 'htmlparser2';
import { getVslotShorthandName } from './vueSlotSyntaxProcessor';
import type { NodeProcessorConfig } from './NodeProcessor';
import { MbNode, NodeOrText } from '../utils/node';

const _ = {
has,
Expand All @@ -12,7 +12,7 @@ const _ = {
/*
* h1 - h6
*/
export function setHeadingId(node: DomElement, config: NodeProcessorConfig) {
export function setHeadingId(node: MbNode, config: NodeProcessorConfig) {
const textContent = cheerio(node).text();
// remove the '&lt;' and '&gt;' symbols that markdown-it uses to escape '<' and '>'
const cleanedContent = textContent.replace(/&lt;|&gt;/g, '');
Expand All @@ -27,16 +27,15 @@ export function setHeadingId(node: DomElement, config: NodeProcessorConfig) {
headerIdMap[slugifiedHeading] = 2;
}

node.attribs = node.attribs ?? {};
node.attribs.id = headerId;
}

/**
* Traverses the dom breadth-first from the specified element to find a html heading child.
* @param node Root element to search from
* @returns {undefined|*} The header element, or undefined if none is found.
* @returns The header element, or undefined if none is found.
*/
function _findHeaderElement(node: DomElement) {
function _findHeaderElement(node: MbNode): undefined | NodeOrText {
const elements = node.children;
if (!elements || !elements.length) {
return undefined;
Expand Down Expand Up @@ -64,15 +63,15 @@ function _findHeaderElement(node: DomElement) {
* This is to ensure anchors still work when panels are in their minimized form.
* @param node The root panel element
*/
export function assignPanelId(node: DomElement) {
export function assignPanelId(node: MbNode) {
const slotChildren = node.children
? node.children.filter(child => getVslotShorthandName(child) !== '')
: [];

const headerSlot = slotChildren.find(child => getVslotShorthandName(child) === 'header');

if (headerSlot) {
const header = _findHeaderElement(headerSlot);
const header = _findHeaderElement(headerSlot as MbNode);
if (!header) {
return;
}
Expand All @@ -82,7 +81,6 @@ export function assignPanelId(node: DomElement) {
+ 'Please report this to the MarkBind developers. Thank you!');
}

node.attribs = node.attribs ?? {};
node.attribs.panelId = header.attribs.id;
}
}
Loading

0 comments on commit 5edcc2f

Please sign in to comment.