diff --git a/docs/devGuide/design/serverSideRendering.md b/docs/devGuide/design/serverSideRendering.md index fd1707e030..bf25dba87a 100644 --- a/docs/devGuide/design/serverSideRendering.md +++ b/docs/devGuide/design/serverSideRendering.md @@ -102,6 +102,12 @@ Some common mistakes are as such: If you are unsure what elements are allowed within other elements, or what constitutes invalid HTML in general, a good resource to reference would be the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span). + +Modern browsers have robust inbuilt mechanisms to auto-correct common causes of hydration. Therefore, to avoid high false positive rate, Markbind has a minimalist rule-based validation against violations. The code for this is located in `core/src/utils/htmlValidationUtils.ts`. + +Please help extend it when violations that cause hydration on browsers are spotted. + + Note that the list only included the common causes of hydration issue that MarkBind developers have ran into. There may be other causes of hydration issue that are not listed here (although unlikely). {% from "njk/common.njk" import previous_next %} diff --git a/packages/core/src/Page/index.ts b/packages/core/src/Page/index.ts index 80043bd689..58256a3838 100644 --- a/packages/core/src/Page/index.ts +++ b/packages/core/src/Page/index.ts @@ -18,6 +18,7 @@ import type { SiteConfig } from '../Site/SiteConfig'; import type { FrontMatter } from '../plugins/Plugin'; import type { ExternalManager } from '../External/ExternalManager'; import { MbNode } from '../utils/node'; +import { checkForVueHydrationViolation } from '../utils/htmlValidationUtil'; import { LAYOUT_DEFAULT_NAME } from '../Layout'; @@ -553,6 +554,7 @@ export class Page { this.collectHeadingsAndKeywords(pageContent); content = `
${content}
`; + checkForVueHydrationViolation(content, this.pageConfig.sourcePath); // Compile the page into Vue application and outputs the render function into script for browser const compiledVuePage = await pageVueServerRenderer.compileVuePageAndCreateScript( diff --git a/packages/core/src/utils/htmlValidationUtil.ts b/packages/core/src/utils/htmlValidationUtil.ts new file mode 100644 index 0000000000..9005cab951 --- /dev/null +++ b/packages/core/src/utils/htmlValidationUtil.ts @@ -0,0 +1,18 @@ +import cheerio from 'cheerio'; +import * as logger from './logger'; + +function logWarningForMissingTbody(rootNode: cheerio.Root, path: string) { + const tables = rootNode('table'); + for (let i = 0; i < tables.length; i += 1) { + const table = rootNode(tables[i]); + if (table.find('tbody').length === 0) { + logger.error(`Invalid HTML in ${path}.\n` + + 'Table must have a tbody tag. Please correct this to avoid Vue hydration issues.\n'); + } + } +} + +export function checkForVueHydrationViolation(content: string, path: string) { + const $ = cheerio.load(content); + logWarningForMissingTbody($, path); +} diff --git a/packages/core/test/unit/utils/HtmlValidationUtil.test.ts b/packages/core/test/unit/utils/HtmlValidationUtil.test.ts new file mode 100644 index 0000000000..471684fa10 --- /dev/null +++ b/packages/core/test/unit/utils/HtmlValidationUtil.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'fs'; +import * as path from 'path'; +import * as logger from '../../../src/utils/logger'; +import { checkForVueHydrationViolation } from '../../../src/utils/htmlValidationUtil'; + +const tableWithoutTbody = readFileSync(path.resolve(__dirname, 'htmlStr/tableWithoutTbody.txt'), 'utf8'); +const tableWithTbody = readFileSync(path.resolve(__dirname, 'htmlStr/tableWithTbody.txt'), 'utf8'); +const correctHtmlString = readFileSync(path.resolve(__dirname, 'htmlStr/correctHtml.txt'), 'utf8'); + +describe('checkForVueHydrationViolation', () => { + it(' should not log an error when the content is correct', () => { + const mockError = jest.spyOn(logger, 'error').mockImplementation(() => {}); + checkForVueHydrationViolation(correctHtmlString, '/fake/path'); + expect(mockError).not.toHaveBeenCalled(); + }); + + it(' should not log an error when all tables have a tbody tag', () => { + const mockError = jest.spyOn(logger, 'error').mockImplementation(() => {}); + checkForVueHydrationViolation(tableWithTbody, '/fake/path'); + expect(mockError).not.toHaveBeenCalled(); + }); + + it(' should log an error when a table does not have a tbody tag', () => { + const mockError = jest.spyOn(logger, 'error').mockImplementation(() => {}); + checkForVueHydrationViolation(tableWithoutTbody, '/fake/path'); + expect(mockError).toHaveBeenCalled(); + const expectedMessange = 'Invalid HTML in /fake/path.\n' + + 'Table must have a tbody tag. Please correct this to avoid Vue hydration issues.\n'; + expect(mockError).toHaveBeenCalledWith(expectedMessange); + }); +}); diff --git a/packages/core/test/unit/utils/htmlStr/correctHtml.txt b/packages/core/test/unit/utils/htmlStr/correctHtml.txt new file mode 100644 index 0000000000..b7e1b1ee18 --- /dev/null +++ b/packages/core/test/unit/utils/htmlStr/correctHtml.txt @@ -0,0 +1,86 @@ +
+
+ + +
  • Topic 1
  • +
  • Topic 2
  • + +
  • Topic 3a
  • +
  • Topic 3b
  • +
    +
    + + + +
    + + +
    +
    +
    +

    Great!
    You've just initialized a MarkBind site.

    +

    Let's get started...

    +
    +

    What just happened?

    +

    You have just initialized a default MarkBind site! It is equipped with a set of core features, including site and page navigation. Additionally, we have included some convenient links to our User Guide, to help you get started quickly and easily.

    + +

    If you were intending to convert an existing GitHub wiki or a docs folder into MarkBind, use the --convert flag instead. See User Guide: MarkBind in the Project Workflow for more information.

    +

    If you want to start with a minimal template instead, use the --template flag with the "minimal" option to initialize a minimal site instead of the default. See User Guide: Templates for more information.

    +
    + +

    This default site comes pre-configured with the core Navigation components: a siteNav, a pageNav, a NavBar, and a Search Bar. To help you get started with the siteNav, we have included five dummy placeholder pages. The NavBar also comes with a placeholder slot for your custom Logo.

    +
    +

    Guide to MarkBind

    +

    To see the capability of MarkBind in action, feel free to take a look at some of the websites built using MarkBind on our Showcase page.

    +

    For more information on how to work with MarkBind sites and to add content, refer to our comprehensive User Guide.

    + +

    If you are interested in contributing to MarkBind, you can refer to our Developer Guide as well!

    + +
    User Guide: Authoring Contents
    +
    +

    Learn about the variety of syntax schemes, formats, and custom MarkBind components that you can use in your MarkBind site.

    +

    More info in: User Guide → Authoring Contents

    +
    +
    User Guide: Working with Sites
    +
    +

    Learn how to modify site properties, apply themes, and enable/disable plugins for your MarkBind site.

    +

    More info in: User Guide → Working with Sites

    +
    +
    User Guide: Full Syntax Reference
    +
    +

    Refer to our Full Syntax Reference page to find a specific feature or component that you want to use in your MarkBind site.

    +

    More info in: User Guide → Full Syntax Reference

    +
    +
    + + +
    +
    \ No newline at end of file diff --git a/packages/core/test/unit/utils/htmlStr/tableWithTbody.txt b/packages/core/test/unit/utils/htmlStr/tableWithTbody.txt new file mode 100644 index 0000000000..011cbfc9df --- /dev/null +++ b/packages/core/test/unit/utils/htmlStr/tableWithTbody.txt @@ -0,0 +1,17 @@ +
    + + + + + + + + + + + + + + +
    Task IDTaskEstimated EffortPrerequisite Task
    EPlanning for next version1 man dayD
    +
    \ No newline at end of file diff --git a/packages/core/test/unit/utils/htmlStr/tableWithoutTbody.txt b/packages/core/test/unit/utils/htmlStr/tableWithoutTbody.txt new file mode 100644 index 0000000000..ef8b354f69 --- /dev/null +++ b/packages/core/test/unit/utils/htmlStr/tableWithoutTbody.txt @@ -0,0 +1,15 @@ +
    + + + + + + + + + + + + +
    Task IDTaskEstimated EffortPrerequisite Task
    EPlanning for next version1 man dayD
    +
    \ No newline at end of file