Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linting for custom markdown syntax #388

Open
5 tasks
GuySartorelli opened this issue Nov 1, 2023 · 0 comments
Open
5 tasks

Add linting for custom markdown syntax #388

GuySartorelli opened this issue Nov 1, 2023 · 0 comments

Comments

@GuySartorelli
Copy link
Member

GuySartorelli commented Nov 1, 2023

Initial linting was added in #11 but excluded linting our custom markdown.

We have custom markdown syntax as documented in extended markdown syntax.

These need to be linted to avoid potential rendering errors, such as forgetting to close a callout block, forgetting to add space around either of these blocks, or trying to use an option that doesn't exist for the children block.

I started trying to do that originally by creating new tokens for the callout block (via a plugin to markdown-it), which I would then need to lint against with a plugin to markdownlint. My attempt at that is below, and it does add those tokens, but it also completely removes the content inside the callout block which means we wouldn't be linting that, which is bad.

Where I got to with implementing a callout block plugin
function calloutBlock(state, startLine, endLine, silent) {
    // TODO validate if these REALLY have to be at the start/end of a line to render
    // TODO If they don't we need to include the tokens regardless so the linter can find and fix them
    const openRegex = /^\s*\[(hint|warning|info|alert|notice|note)\]/;
    const closeRegex = /\[\/(hint|warning|info|alert|notice|note)\]\s*$/;

    // if it's indented more than 3 spaces, it should be a code block
    if (state.sCount[startLine] - state.blkIndent > 3) {
        return false;
    }

    // if it's inside a code block, we don't parse it
    // this shouldn't ever get hit because of the way the parser works - it's mostly here for
    // potential future-proofing
    if (state.parentType === 'code') {
        return false;
    }

    // where the line starts + any whitespace at the start of the line
    const pos = state.bMarks[startLine] + state.tShift[startLine];
    // where the line ends
    const end = state.eMarks[startLine];

    const lineText = state.src.slice(pos, end);

    // if we don't find an opening tag, we bail
    if (!lineText.match(openRegex)) {
        return false;
    }

    // if the parser is set to validation-only mode, we can stop here, which is
    // potentially useful for debugging
    if (silent) {
        return true;
    }

    const oldParentType = state.parentType;
    state.parentType = 'callout';
    let hasEndMarker = false;

    const tokenOpen = state.push('callout_open', 'div', 1);
    tokenOpen.info = ''; // TODO probably put the type in here - or maybe that's what meta is for?
    tokenOpen.markup = ''; // TODO grab the full [info]
    let itemlines;
    tokenOpen.map = itemlines = [ startLine, 0 ];//state.line ];

    // search for the end of the block
    let nextLine = startLine;
    while (nextLine < endLine) {

        // mark the true start and end of the next line
        let cursor = state.bMarks[nextLine] + state.tShift[nextLine];
        let max = state.eMarks[nextLine];

        // ignore anything that md will interpret as starting a code block
        if (state.sCount[nextLine] - state.blkIndent <= 3) {
            let nextLineText = state.src.slice(cursor, max);

            if (nextLineText.match(closeRegex)) {
                hasEndMarker = true;
                break;
            }
        }

        nextLine++;
    }
    itemlines[1] = nextLine;

    // if we found an end marker, start processing again _after_ that point.
    state.line = nextLine + (hasEndMarker ? 1 : 0);

    if (hasEndMarker) {
        const tokenClose = state.push('callout_close', 'div', -1);
        tokenClose.info = ''; // TODO probably put the type in here
        tokenClose.markup = ''; // TODO grab the full [/info]
    }

    state.parentType = oldParentType;

    console.log(state);

    return true;
}

export default function callout_block_plugin(md) {
    md.block.ruler.before('paragraph', 'ss_callout_block', calloutBlock);
}

It would work find for children blocks but isn't suitable for callout blocks, so I recommend we change the syntax we're using for callout blocks.

Linting callout blocks may require changing our syntax for rendering them

If there's a plugin for markdown-it for the GitHub alert syntax then I recommend we swap to that. It also makes the docs more portable (they'll render nicely anywhere that supports that syntax, including directly on GitHub, without us needing to re-implement rendering it).

If that's not an option, I recommend instead that we change the syntax for the callout and children blocks to use XML syntax which I think would be immediately lintable (e.g. [info]my info content[/info] would become <callout type="info">my info content</callout>).

NOTE that any change to how these are rendered would also affect CMS 3 docs. We'd either have to update those, or keep the legacy rendering style in addition to the new one.

Acceptance criteria

  • If we are changing how our callout blocks are rendered, that is done first, probably as a separate card which blocks this one
  • Callout blocks are linted
    • Must have an empty line before the starting tag
    • Must have an empty line after the closing tag
    • Must not have any other content on the same line as the opening or closing tags
    • All opened callout blocks must be closed
    • Callout blocks must not be added inside another callout block, a blockquote, a list, or a table (basically, these can't be inside any block element other than a paragraph)
  • Children blocks are linted
    • Must have an empty line before and after the children block
    • Children block must not span multiple lines
    • Must not have any other content on the same line as the children block
    • No unnecessary spaces are inside the children block
    • No unexpected options are used inside the children block
    • Children blocks must not be added inside a callout block, a blockquote, a list, or a table (basically, these can't be inside any block element other than a paragraph)
  • relative links (i.e. links to additional docs pages) are linted
    • Always include a trailing slash (if there's an anchor, the trailing slash is before the anchor)
    • Always start from / or be at the same level as the current page (i.e. no ../my_page)
    • Page being linked to must exist
    • If there's an anchor, the anchor must exist on the page being linked to
    • No absolute URLs (unless we're explicitly intentionally linking to docs from a different version - those should use an HTML comment to skip the linting rule for that URL)
  • API links are linted
    • Don't use the shorthand [api:MyNamespace\MyClass] - always use [`MyClass`](api:MyNamespace\MyClass)
    • Validate the syntax for the api link is correct (e.g. use MyNamespace\MyClass->propertyName for properties and config - never MyNamespace\MyClass.configName)
    • No absolute URLs
    • code in the text part of the link has backticks (e.g. [MyClass](api:MyNamespace\MyClass) is invalid, use [`MyClass`](api:MyNamespace\MyClass) instead)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant