diff --git a/.github/actions-scripts/rendered-content-link-checker.js b/.github/actions-scripts/rendered-content-link-checker.js index 2d28371f35..643197236e 100755 --- a/.github/actions-scripts/rendered-content-link-checker.js +++ b/.github/actions-scripts/rendered-content-link-checker.js @@ -14,7 +14,7 @@ import contextualize from '../../middleware/context.js' import features from '../../middleware/contextualizers/features.js' import getRedirect from '../../lib/get-redirect.js' import warmServer from '../../lib/warm-server.js' -import liquid from '../../lib/render-content/liquid.js' +import { liquid } from '../../src/content-render/index.js' import { deprecated } from '../../lib/enterprise-server-releases.js' import excludedLinks from '../../lib/excluded-links.js' import { getEnvInputs, boolEnvVar } from './lib/get-env-inputs.js' @@ -22,7 +22,7 @@ import { debugTimeEnd, debugTimeStart } from './lib/debug-time-taken.js' import { uploadArtifact as uploadArtifactLib } from './lib/upload-artifact.js' import github from '../../script/helpers/github.js' import { getActionContext } from './lib/action-context.js' -import { createMinimalProcessor } from '../../lib/render-content/create-processor.js' +import { createMinimalProcessor } from '../../src/content-render/unified/processor.js' const STATIC_PREFIXES = { assets: path.resolve('assets'), diff --git a/lib/get-mini-toc-items.js b/lib/get-mini-toc-items.js index ceaf9b4ca1..abb3c88728 100644 --- a/lib/get-mini-toc-items.js +++ b/lib/get-mini-toc-items.js @@ -1,7 +1,7 @@ import cheerio from 'cheerio' import { range } from 'lodash-es' -import renderContent from './render-content/index.js' +import { renderContent } from '#src/content-render/index.js' // Keep maxHeadingLevel=2 for accessibility reasons, see docs-engineering#2701 for more info export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope = '') { diff --git a/lib/page.js b/lib/page.js index 0d55097842..3bcdd6783b 100644 --- a/lib/page.js +++ b/lib/page.js @@ -9,7 +9,7 @@ import getTocItems from './get-toc-items.js' import pathUtils from './path-utils.js' import Permalink from './permalink.js' import languages from './languages.js' -import renderContent from './render-content/index.js' +import { renderContent } from '#src/content-render/index.js' import processLearningTracks from '#src/learning-track/lib/process-learning-tracks.js' import { productMap } from './all-products.js' import slash from 'slash' diff --git a/lib/release-notes-utils.js b/lib/release-notes-utils.js index 32df3ff425..212e35758f 100644 --- a/lib/release-notes-utils.js +++ b/lib/release-notes-utils.js @@ -1,5 +1,5 @@ import semver from 'semver' -import renderContent from './render-content/index.js' +import { renderContent } from '#src/content-render/index.js' /** * Create an array of release note objects and sort them by number. diff --git a/lib/render-content/README.md b/lib/render-content/README.md deleted file mode 100644 index 2caef2af77..0000000000 --- a/lib/render-content/README.md +++ /dev/null @@ -1,48 +0,0 @@ -Markdown and Liquid rendering pipeline. - -## Usage - -```js -const renderContent = require('.') - -const html = await renderContent(` -# Beep -{{ foo }} -`, { - foo: 'bar' -}) -``` - -Creates: - -```html -
bar
-``` - -## API - -### renderContent(markdown, context = {}, options = {}) - -Render a string of `markdown` with optional `context`. Returns a `Promise`. - -Liquid will be looking for includes in `${process.cwd()}/includes`. - -Options: - -- `fileName`: File name for debugging purposes. -- `textOnly`: Output text instead of html using [cheerio](https://ghub.io/cheerio). - -### .liquid - -The [Liquid](https://ghub.io/liquidjs) instance used internally. - -### Code block headers - -You can add a header to code blocks by adding the ` copy` annotation after the code fences, and a specified language: - - ```js copy - const copyMe = true - ``` - -The un-highlighted text is available as `button.js-btn-copy`'s `data-clipboard-text` attribute. diff --git a/lib/render-content/index.js b/lib/render-content/index.js deleted file mode 100644 index 10c74ee33e..0000000000 --- a/lib/render-content/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import GithubSlugger from 'github-slugger' -import renderContent from './renderContent.js' -import { ExtendedMarkdown, tags } from '../liquid-tags/extended-markdown.js' -import IndentedDataReference from '../liquid-tags/indented-data-reference.js' -import Data from '../liquid-tags/data.js' -import Octicon from '../liquid-tags/octicon.js' -import Ifversion from '../liquid-tags/ifversion.js' - -renderContent.liquid.registerTag('indented_data_reference', IndentedDataReference) -renderContent.liquid.registerTag('data', Data) -renderContent.liquid.registerTag('octicon', Octicon) -renderContent.liquid.registerTag('ifversion', Ifversion) - -for (const tag in tags) { - // Register all the extended markdown tags, like {% note %} and {% warning %} - renderContent.liquid.registerTag(tag, ExtendedMarkdown) -} - -/** - * Like the `size` filter, but specifically for - * getting the number of keys in an object - */ -renderContent.liquid.registerFilter('obj_size', (input) => { - if (!input) return 0 - return Object.keys(input).length -}) - -/** - * Returns the version number of a GHES version string - * ex: enterprise-server@2.22 => 2.22 - */ -renderContent.liquid.registerFilter('version_num', (input) => { - return input.split('@')[1] -}) - -/** - * Convert the input to a slug - */ -renderContent.liquid.registerFilter('slugify', (input) => { - const slugger = new GithubSlugger() - return slugger.slug(input) -}) - -export default renderContent - -export const liquid = renderContent.liquid diff --git a/lib/render-content/liquid.js b/lib/render-content/liquid.js deleted file mode 100644 index b87e306512..0000000000 --- a/lib/render-content/liquid.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Liquid } from 'liquidjs' - -const engine = new Liquid({ - extname: '.html', - dynamicPartials: false, -}) - -export default engine diff --git a/lib/render-content/renderContent.js b/lib/render-content/renderContent.js deleted file mode 100644 index 071eb8645a..0000000000 --- a/lib/render-content/renderContent.js +++ /dev/null @@ -1,79 +0,0 @@ -import liquid from './liquid.js' -import cheerio from 'cheerio' -import { decode } from 'html-entities' -import stripHtmlComments from 'strip-html-comments' -import createProcessor from './create-processor.js' - -// used below to remove extra newlines in TOC lists -const endLine = '\r?\n' -const blankLine = '\\s*?[\r\n]*' -const startNextLine = '[^\\S\r\n]*?[-\\*] and entities like < - template = template.replace( - /``` ?shell\r?\n\s*?(\S[\s\S]*?)\r?\n.*?```/gm, - '$1'
- )
-
- // clean up empty lines in TOC lists left by unrendered list items (due to productVersions)
- // for example, remove the blank line here:
- // - foo
- //
- // - bar
- if (template.includes('')) {
- template = template.replace(blankLineInList, '$1$2')
- }
-
- // this removes any extra newlines left by (now resolved) liquid
- // statements so that extra space doesn't mess with list numbering
- template = template.replace(/(\r?\n){3}/g, '\n\n')
-
- const processor = createProcessor(context)
- const vFile = await processor.process(template)
- let html = vFile.toString()
-
- if (options.textOnly) {
- html = fastTextOnly(html)
- }
-
- return html.trim()
- } catch (error) {
- if (options.filename) {
- console.error(`renderContent failed on file: ${options.filename}`)
- }
- throw error
- }
-}
-
-// Given a piece of HTML return it without HTML. E.g.
-// `Foo & bar
` becomes `Foo & bar` -// and `A link andcode` becomes `A link and code`.
-// Take advantage of the subtle fact that a lot of the times, the html value
-// we get here is a single line that starts with `` and ends with `
` -// and contains no longer HTML tags. -function fastTextOnly(html) { - if (!html) return '' - if (html.startsWith('') && html.endsWith('
')) { - const middle = html.slice(3, -4) - if (!middle.includes('<')) return decode(middle.trim()) - } - return cheerio.load(html, { xmlMode: true }).text().trim() -} - -renderContent.liquid = liquid - -export default renderContent diff --git a/lib/render-with-fallback.js b/lib/render-with-fallback.js index 42e052033a..a2def19648 100644 --- a/lib/render-with-fallback.js +++ b/lib/render-with-fallback.js @@ -1,6 +1,6 @@ -import renderContent from './render-content/index.js' +import { renderContent } from '#src/content-render/index.js' import Page from './page.js' -import { TitleFromAutotitleError } from './render-content/plugins/rewrite-local-links.js' +import { TitleFromAutotitleError } from '#src/content-render/unified/rewrite-local-links.js' class EmptyTitleError extends Error {} diff --git a/middleware/contextualizers/current-product-tree.js b/middleware/contextualizers/current-product-tree.js index 552dd08a8e..8dcc03862b 100644 --- a/middleware/contextualizers/current-product-tree.js +++ b/middleware/contextualizers/current-product-tree.js @@ -1,5 +1,5 @@ import path from 'path' -import liquid from '../../lib/render-content/liquid.js' +import { liquid } from '#src/content-render/index.js' import findPageInSiteTree from '../../lib/find-page-in-site-tree.js' import removeFPTFromPath from '../../lib/remove-fpt-from-path.js' import { executeWithFallback } from '../../lib/render-with-fallback.js' diff --git a/middleware/contextualizers/glossaries.js b/middleware/contextualizers/glossaries.js index c55ff24409..c312bbfec6 100644 --- a/middleware/contextualizers/glossaries.js +++ b/middleware/contextualizers/glossaries.js @@ -1,5 +1,5 @@ import { getDataByLanguage } from '../../lib/get-data.js' -import liquid from '../../lib/render-content/liquid.js' +import { liquid } from '#src/content-render/index.js' import { executeWithFallback } from '../../lib/render-with-fallback.js' import { correctTranslatedContentStrings } from '../../lib/correct-translation-content.js' diff --git a/script/dev-toc/generate.js b/script/dev-toc/generate.js index 272328c90f..f390b25edc 100755 --- a/script/dev-toc/generate.js +++ b/script/dev-toc/generate.js @@ -6,7 +6,7 @@ import { execSync } from 'child_process' import { program } from 'commander' import fpt from '../../lib/non-enterprise-default-version.js' import { allVersionKeys } from '../../lib/all-versions.js' -import { liquid } from '../../lib/render-content/index.js' +import { liquid } from '#src/content-render/index.js' import contextualize from '../../middleware/context.js' const layoutFilename = path.posix.join(process.cwd(), 'script/dev-toc/layout.html') diff --git a/script/reconcile-category-dirs-with-ids.js b/script/reconcile-category-dirs-with-ids.js index d694d3d0e5..c397f73e09 100755 --- a/script/reconcile-category-dirs-with-ids.js +++ b/script/reconcile-category-dirs-with-ids.js @@ -19,7 +19,7 @@ import walk from 'walk-sync' import slash from 'slash' import GithubSlugger from 'github-slugger' import { decode } from 'html-entities' -import renderContent from '../lib/render-content/index.js' +import { renderContent } from '#src/content-render/index.js' const slugger = new GithubSlugger() diff --git a/src/content-linter/lib/linting-rules/image-alt-text-length.js b/src/content-linter/lib/linting-rules/image-alt-text-length.js index a779c3c17e..a55c56ae9f 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-length.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-length.js @@ -1,5 +1,5 @@ import { addError, forEachInlineChild } from 'markdownlint-rule-helpers' -import renderContent from '../../../../lib/render-content/index.js' +import { renderContent } from '#src/content-render/index.js' export const incorrectAltTextLength = { names: ['MD111', 'incorrect-alt-text-length'], diff --git a/src/content-linter/tests/category-pages.js b/src/content-linter/tests/category-pages.js index ab2f603d32..623f561a3a 100644 --- a/src/content-linter/tests/category-pages.js +++ b/src/content-linter/tests/category-pages.js @@ -7,7 +7,7 @@ import GithubSlugger from 'github-slugger' import { decode } from 'html-entities' import matter from '../../../lib/read-frontmatter.js' -import renderContent from '../../../lib/render-content/index.js' +import { renderContent } from '#src/content-render/index.js' import getApplicableVersions from '../../../lib/get-applicable-versions.js' import contextualize from '../../../middleware/context.js' import shortVersions from '../../../middleware/contextualizers/short-versions.js' diff --git a/src/content-linter/tests/lint-files.js b/src/content-linter/tests/lint-files.js index 18d92ce029..38b3094e3e 100755 --- a/src/content-linter/tests/lint-files.js +++ b/src/content-linter/tests/lint-files.js @@ -16,10 +16,10 @@ import { jest } from '@jest/globals' import { frontmatter, deprecatedProperties } from '../../../lib/frontmatter.js' import languages from '../../../lib/languages.js' -import { tags } from '../../../lib/liquid-tags/extended-markdown.js' +import { tags } from '#src/content-render/liquid/extended-markdown.js' import releaseNotesSchema from '../lib/release-notes-schema.js' import learningTracksSchema from '../lib/learning-tracks-schema.js' -import renderContent from '../../../lib/render-content/index.js' +import { renderContent } from '#src/content-render/index.js' import getApplicableVersions from '../../../lib/get-applicable-versions.js' import { allVersions } from '../../../lib/all-versions.js' import { getDiffFiles } from '../lib/diff-files.js' diff --git a/src/content-linter/tests/lint-versioning.js b/src/content-linter/tests/lint-versioning.js index 6d3e696350..7f4810a1e4 100644 --- a/src/content-linter/tests/lint-versioning.js +++ b/src/content-linter/tests/lint-versioning.js @@ -7,7 +7,7 @@ import semver from 'semver' import { allVersions, allVersionShortnames } from '../../../lib/all-versions.js' import { supported, next, nextNext, deprecated } from '../../../lib/enterprise-server-releases.js' import { getLiquidConditionals } from '../../../script/helpers/get-liquid-conditionals.js' -import allowedVersionOperators from '../../../lib/liquid-tags/ifversion-supported-operators.js' +import allowedVersionOperators from '#src/content-render/liquid/ifversion-supported-operators.js' import featureVersionsSchema from '../lib/feature-versions-schema.js' import walkFiles from '../../../script/helpers/walk-files.js' import { getDeepDataByLanguage } from '../../../lib/get-data.js' diff --git a/lib/liquid-tags/README.md b/src/content-render/README.md similarity index 54% rename from lib/liquid-tags/README.md rename to src/content-render/README.md index 45db45415a..204d7cefce 100644 --- a/lib/liquid-tags/README.md +++ b/src/content-render/README.md @@ -1,4 +1,55 @@ -# Liquid Tags +# Render content + +In this directory is the main pipeline that converts our content from Liquid, Markdown and YAML into HTML. This directory _does not include React components_. + +## Usage + +```js +const renderContent = require('.') + +const html = await renderContent(` +# Beep +{{ foo }} +`, { + foo: 'bar' +}) +``` + +Creates: + +```html +bar
+``` + +## API + +### renderContent(markdown, context = {}, options = {}) + +Render a string of `markdown` with optional `context`. Returns a `Promise`. + +Liquid will be looking for includes in `${process.cwd()}/includes`. + +Options: + +- `fileName`: File name for debugging purposes. +- `textOnly`: Output text instead of html using [cheerio](https://ghub.io/cheerio). + +### .liquid + +The [Liquid](https://ghub.io/liquidjs) instance used internally. + +### Code block headers + +You can add a header to code blocks by adding the ` copy` annotation after the code fences, and a specified language: + + ```js copy + const copyMe = true + ``` + +The un-highlighted text is available as `button.js-btn-copy`'s `data-clipboard-text` attribute. + +## Liquid tags See also [contributing/liquid-helpers.md](../../contributing/liquid-helpers.md) @@ -36,7 +87,7 @@ Each custom tag has the following: The class and the template should have corresponding names, like `lib/liquid-tags/my-tag.js` and `includes/liquid-tags/my-tag.html` -You must also register the new tag in `lib/render-content/index.js` with a line like this: +You must also register the new tag in `src/content-render/liquid/engine.js` with a line like this: ``` renderContent.liquid.registerTag('my_tag', require('./liquid-tags/my-tag')) diff --git a/src/content-render/index.js b/src/content-render/index.js new file mode 100644 index 0000000000..7ccb169a09 --- /dev/null +++ b/src/content-render/index.js @@ -0,0 +1,22 @@ +import { renderLiquid } from './liquid/index.js' +import { renderUnified } from './unified/index.js' +import { engine } from './liquid/engine.js' + +// parse multiple times because some templates contain more templates. :] +export async function renderContent(template = '', context = {}, options = {}) { + // If called with a falsy template, it can't ever become something + // when rendered. We can exit early to save some pointless work. + if (!template) return template + try { + template = await renderLiquid(template, context) + const html = await renderUnified(template, context, options) + return html + } catch (error) { + if (options.filename) { + console.error(`renderContent failed on file: ${options.filename}`) + } + throw error + } +} + +export const liquid = engine diff --git a/lib/liquid-tags/data.js b/src/content-render/liquid/data.js similarity index 93% rename from lib/liquid-tags/data.js rename to src/content-render/liquid/data.js index 9f4c0fbe37..0cd501dd67 100644 --- a/lib/liquid-tags/data.js +++ b/src/content-render/liquid/data.js @@ -1,7 +1,7 @@ import { TokenizationError } from 'liquidjs' import { THROW_ON_EMPTY, DataReferenceError } from './error-handling.js' -import { getDataByLanguage } from '../get-data.js' +import { getDataByLanguage } from '../../../lib/get-data.js' const Syntax = /([a-z0-9/\\_.\-[\]]+)/i const SyntaxHelp = "Syntax Error in 'data' - Valid syntax: data [path]" diff --git a/src/content-render/liquid/engine.js b/src/content-render/liquid/engine.js new file mode 100644 index 0000000000..f75dd2784a --- /dev/null +++ b/src/content-render/liquid/engine.js @@ -0,0 +1,47 @@ +import { Liquid } from 'liquidjs' +import GithubSlugger from 'github-slugger' +import IndentedDataReference from './indented-data-reference.js' +import Data from './data.js' +import Octicon from './octicon.js' +import Ifversion from './ifversion.js' +import { ExtendedMarkdown, tags } from './extended-markdown.js' + +export const engine = new Liquid({ + extname: '.html', + dynamicPartials: false, +}) + +engine.registerTag('indented_data_reference', IndentedDataReference) +engine.registerTag('data', Data) +engine.registerTag('octicon', Octicon) +engine.registerTag('ifversion', Ifversion) + +for (const tag in tags) { + // Register all the extended markdown tags, like {% note %} and {% warning %} + engine.registerTag(tag, ExtendedMarkdown) +} + +/** + * Like the `size` filter, but specifically for + * getting the number of keys in an object + */ +engine.registerFilter('obj_size', (input) => { + if (!input) return 0 + return Object.keys(input).length +}) + +/** + * Returns the version number of a GHES version string + * ex: enterprise-server@2.22 => 2.22 + */ +engine.registerFilter('version_num', (input) => { + return input.split('@')[1] +}) + +/** + * Convert the input to a slug + */ +engine.registerFilter('slugify', (input) => { + const slugger = new GithubSlugger() + return slugger.slug(input) +}) diff --git a/lib/liquid-tags/error-handling.js b/src/content-render/liquid/error-handling.js similarity index 100% rename from lib/liquid-tags/error-handling.js rename to src/content-render/liquid/error-handling.js diff --git a/lib/liquid-tags/extended-markdown.js b/src/content-render/liquid/extended-markdown.js similarity index 96% rename from lib/liquid-tags/extended-markdown.js rename to src/content-render/liquid/extended-markdown.js index 216273376a..ecd3dd4385 100644 --- a/lib/liquid-tags/extended-markdown.js +++ b/src/content-render/liquid/extended-markdown.js @@ -1,4 +1,4 @@ -import { allTools } from '../all-tools.js' +import { allTools } from '../../../lib/all-tools.js' // we do this to get an object that combines all possible liquid tags const toolTags = Object.fromEntries(Object.keys(allTools).map((tool) => [tool, ''])) diff --git a/lib/liquid-tags/ifversion-supported-operators.js b/src/content-render/liquid/ifversion-supported-operators.js similarity index 100% rename from lib/liquid-tags/ifversion-supported-operators.js rename to src/content-render/liquid/ifversion-supported-operators.js diff --git a/lib/liquid-tags/ifversion.js b/src/content-render/liquid/ifversion.js similarity index 98% rename from lib/liquid-tags/ifversion.js rename to src/content-render/liquid/ifversion.js index 439ab1cd8a..70fbfead41 100644 --- a/lib/liquid-tags/ifversion.js +++ b/src/content-render/liquid/ifversion.js @@ -1,5 +1,5 @@ import { Tag, isTruthy, Value, TokenizationError } from 'liquidjs' -import versionSatisfiesRange from '../version-satisfies-range.js' +import versionSatisfiesRange from '../../../lib/version-satisfies-range.js' import supportedOperators from './ifversion-supported-operators.js' const SyntaxHelp = diff --git a/lib/liquid-tags/indented-data-reference.js b/src/content-render/liquid/indented-data-reference.js similarity index 97% rename from lib/liquid-tags/indented-data-reference.js rename to src/content-render/liquid/indented-data-reference.js index 2635622097..8c1954a118 100644 --- a/lib/liquid-tags/indented-data-reference.js +++ b/src/content-render/liquid/indented-data-reference.js @@ -1,7 +1,7 @@ import assert from 'assert' import { THROW_ON_EMPTY, IndentedDataReferenceError } from './error-handling.js' -import { getDataByLanguage } from '../get-data.js' +import { getDataByLanguage } from '../../../lib/get-data.js' // This class supports a tag that expects two parameters, a data reference and `spaces=NUMBER`: // diff --git a/src/content-render/liquid/index.js b/src/content-render/liquid/index.js new file mode 100644 index 0000000000..e1b4386abf --- /dev/null +++ b/src/content-render/liquid/index.js @@ -0,0 +1,10 @@ +import { processLiquidPre } from './pre.js' +import { processLiquidPost } from './post.js' +import { engine } from './engine.js' + +export async function renderLiquid(template, context) { + template = processLiquidPre(template) + template = await engine.parseAndRender(template, context) + template = processLiquidPost(template) + return template +} diff --git a/lib/liquid-tags/octicon.js b/src/content-render/liquid/octicon.js similarity index 100% rename from lib/liquid-tags/octicon.js rename to src/content-render/liquid/octicon.js diff --git a/src/content-render/liquid/post.js b/src/content-render/liquid/post.js new file mode 100644 index 0000000000..431f1319b6 --- /dev/null +++ b/src/content-render/liquid/post.js @@ -0,0 +1,40 @@ +// used below to remove extra newlines in TOC lists +const endLine = '\r?\n' +const blankLine = '\\s*?[\r\n]*' +const startNextLine = '[^\\S\r\n]*?[-\\*] and entities like < + template = template.replace( + /``` ?shell\r?\n\s*?(\S[\s\S]*?)\r?\n.*?```/gm, + '$1'
+ )
+ return template
+}
+
+function cleanUpListEmptyLines(template) {
+ // clean up empty lines in TOC lists left by unrendered list items (due to productVersions)
+ // for example, remove the blank line here:
+ // - foo
+ //
+ // - bar
+ if (template.includes('')) {
+ template = template.replace(blankLineInList, '$1$2')
+ }
+ return template
+}
+
+function cleanUpExtraEmptyLines(template) {
+ // this removes any extra newlines left by (now resolved) liquid
+ // statements so that extra space doesn't mess with list numbering
+ template = template.replace(/(\r?\n){3}/g, '\n\n')
+ return template
+}
diff --git a/src/content-render/liquid/pre.js b/src/content-render/liquid/pre.js
new file mode 100644
index 0000000000..1d9f1a1d7a
--- /dev/null
+++ b/src/content-render/liquid/pre.js
@@ -0,0 +1,13 @@
+import stripHtmlComments from 'strip-html-comments'
+
+export function processLiquidPre(template) {
+ template = removeHtmlComments(template)
+ return template
+}
+
+function removeHtmlComments(template) {
+ // remove any newlines that precede html comments, then remove the comments
+ template = template.replace(/\n