diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 831406539..41fe83806 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -94,7 +94,6 @@ jobs:
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('asssets/package-lock.json') }}
- - run: mkdir -p tmp/handlebars
- run: npm ci --prefix assets
- run: npm run build --prefix assets
- name: Push updated assets
diff --git a/assets/.eslintrc.js b/assets/.eslintrc.js
index 7436d4593..b36d1cfd2 100644
--- a/assets/.eslintrc.js
+++ b/assets/.eslintrc.js
@@ -16,8 +16,5 @@ module.exports = {
'no-throw-literal': 0,
'no-useless-escape': 0,
'object-curly-spacing': 0
- },
- globals: {
- 'Handlebars': 'readonly'
}
}
diff --git a/assets/build/build.js b/assets/build/build.js
index b490cd034..c46619056 100644
--- a/assets/build/build.js
+++ b/assets/build/build.js
@@ -1,155 +1,89 @@
const path = require('node:path')
const process = require('node:process')
-const child_process = require('node:child_process')
+const cp = require('node:child_process')
const esbuild = require('esbuild')
-const util = require('./utilities')
-
-const watchMode = Boolean(process.env.npm_config_watch)
-
-
-/**
- * Configuration variables
- */
-
-// Basic build configuration and values
-const commonOptions = {
- entryNames: '[name]-[hash]',
- bundle: true,
- minify: true,
- logLevel: watchMode ? 'warning' : 'info',
-}
-const epubOutDir = path.resolve('../formatters/epub/dist')
-const htmlOutDir = path.resolve('../formatters/html/dist')
-
-// Handlebars template paths
-const templates = {
- sourceDir: path.resolve('js/handlebars/templates'),
- compiledDir: path.resolve('../tmp/handlebars'),
- filename: 'handlebars.templates.js',
-}
-templates.compiledPath = path.join(templates.compiledDir, templates.filename)
-
-
-/**
- * Build: Plugins
- */
-
-// Empty outdir directories before both normal and watch-mode builds
-const epubOnStartPlugin = {
- name: 'epubOnStart',
- setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([epubOutDir])) },
-}
-const htmlOnStartPlugin = {
- name: 'htmlOnStart',
- setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([htmlOutDir])) },
-}
+const fsExtra = require('fs-extra')
+const fs = require('node:fs/promises')
+const handlebars = require('handlebars')
+const util = require('node:util')
+const exec = util.promisify(cp.exec)
-/**
- * Build
- */
-
-// ePub: esbuild options
-const epubBuildOptions = {
- ...commonOptions,
- outdir: epubOutDir,
- plugins: [epubOnStartPlugin],
- entryPoints: [
- 'js/entry/epub.js',
- 'css/entry/epub-elixir.css',
- 'css/entry/epub-erlang.css',
- ],
-}
-
-// ePub: esbuild (conditionally configuring watch mode and rebuilding of docs)
-if (!watchMode) {
- esbuild.build(epubBuildOptions).catch(() => process.exit(1))
-} else {
- esbuild.build({
- ...epubBuildOptions,
- watch: {
- onRebuild(error, result) {
- if (error) {
- console.error('[watch] epub build failed:', error)
- } else {
- console.log('[watch] epub assets rebuilt')
- if (result.errors.length > 0) console.log('[watch] epub build errors:', result.errors)
- if (result.warnings.length > 0) console.log('[watch] epub build warnings:', result.warnings)
- generateDocs("epub")
- }
- },
- },
- }).then(() => generateDocs("epub")).catch(() => process.exit(1))
-}
-
-// HTML: Precompile Handlebars templates
-util.runShellCmdSync(`npx handlebars ${templates.sourceDir} --output ${templates.compiledPath}`)
+const watchMode = Boolean(process.env.npm_config_watch)
-// HTML: esbuild options
-const htmlBuildOptions = {
- ...commonOptions,
- outdir: htmlOutDir,
- plugins: [htmlOnStartPlugin],
- entryPoints: [
- templates.compiledPath,
- 'js/entry/html.js',
- 'css/entry/html-elixir.css',
- 'css/entry/html-erlang.css',
- ],
- loader: {
- '.woff2': 'file',
- // TODO: Remove when @fontsource/* removes legacy .woff
- '.woff': 'file',
+/** @type {import('esbuild').BuildOptions[]} */
+const formatters = [
+ {
+ formatter: 'epub',
+ outdir: path.resolve('../formatters/epub/dist'),
+ entryPoints: [
+ 'js/entry/epub.js',
+ 'css/entry/epub-elixir.css',
+ 'css/entry/epub-erlang.css'
+ ]
},
-}
-
-// HTML: esbuild (conditionally configuring watch mode and rebuilding of docs)
-if (!watchMode) {
- esbuild.build(htmlBuildOptions).then(() => buildTemplatesRuntime()).catch(() => process.exit(1))
-} else {
- esbuild.build({
- ...htmlBuildOptions,
- watch: {
- onRebuild(error, result) {
- if (error) {
- console.error('[watch] html build failed:', error)
- } else {
- console.log('[watch] html assets rebuilt')
- if (result.errors.length > 0) console.log('[watch] html build errors:', result.errors)
- if (result.warnings.length > 0) console.log('[watch] html build warnings:', result.warnings)
- buildTemplatesRuntime()
- generateDocs("html")
+ {
+ formatter: 'html',
+ outdir: path.resolve('../formatters/html/dist'),
+ entryPoints: [
+ 'js/entry/html.js',
+ 'css/entry/html-elixir.css',
+ 'css/entry/html-erlang.css'
+ ],
+ loader: {
+ '.woff2': 'file',
+ // TODO: Remove when @fontsource/* removes legacy .woff
+ '.woff': 'file'
+ }
+ }
+]
+
+Promise.all(formatters.map(async ({formatter, ...options}) => {
+ // Clean outdir.
+ await fsExtra.emptyDir(options.outdir)
+
+ await esbuild.build({
+ entryNames: watchMode ? '[name]-dev' : '[name]-[hash]',
+ bundle: true,
+ minify: !watchMode,
+ logLevel: watchMode ? 'warning' : 'info',
+ watch: watchMode,
+ ...options,
+ plugins: [{
+ name: 'ex_doc',
+ setup (build) {
+ // Pre-compile handlebars templates.
+ build.onLoad({
+ filter: /\.handlebars$/
+ }, async ({ path: filename }) => {
+ try {
+ const source = await fs.readFile(filename, 'utf-8')
+ const template = handlebars.precompile(source)
+ const contents = [
+ "import * as Handlebars from 'handlebars/runtime'",
+ "import '../helpers'",
+ `export default Handlebars.template(${template})`
+ ].join('\n')
+ return { contents }
+ } catch (error) {
+ return { errors: [{ text: error.message }] }
+ }
+ })
+
+ // Generate docs with new assets (watch mode only).
+ if (watchMode) {
+ build.onEnd(async result => {
+ if (result.errors.length) return
+ console.log(`${formatter} assets built`)
+ await exec('mix compile --force', {cwd: '../'})
+ await exec(`mix docs --formatter ${formatter}`, {cwd: '../'})
+ console.log(`${formatter} docs built`)
+ })
}
- },
- },
- }).then(() => {
- buildTemplatesRuntime()
- generateDocs("html")
- }).catch(() => process.exit(1))
-}
-
-/**
- * Functions
- */
-
-// HTML: Handlebars runtime
-// The Handlebars runtime from the local module dist directory is used to ensure
-// the version matches that which was used to compile the templates.
-// 'bundle' must be false in order for 'Handlebar' to be available at runtime.
-function buildTemplatesRuntime() {
- esbuild.build({
- ...commonOptions,
- outdir: htmlOutDir,
- entryPoints: ['node_modules/handlebars/dist/handlebars.runtime.js'],
- bundle: false,
- }).catch(() => process.exit(1))
-}
-
-// Docs generation (used in watch mode only)
-function generateDocs(formatter) {
- console.log(`Building ${formatter} docs`)
- process.chdir('../')
- child_process.execSync('mix compile --force')
- child_process.execSync(`mix docs --formatter ${formatter}`)
- process.chdir('./assets/')
-}
+ }
+ }]
+ })
+})).catch((error) => {
+ console.error(error)
+ process.exit(1)
+})
diff --git a/assets/build/utilities.js b/assets/build/utilities.js
deleted file mode 100644
index 9ac0d11a7..000000000
--- a/assets/build/utilities.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const child_process = require('node:child_process')
-const fs = require('fs-extra')
-
-module.exports.runShellCmdSync = (command) => {
- child_process.execSync(command, (err, stdout, stderr) => {
- if (err) {
- console.error(err)
- process.exit(1)
- } else {
- if (stdout) { console.log('\n' + stdout) }
- if (stderr) { console.log('\n' + stderr) }
- return true
- }
- })
-}
-
-module.exports.ensureEmptyDirsExistSync = (dirs) => {
- dirs.forEach(dir => {
- fs.emptyDirSync(dir)
- })
-}
diff --git a/assets/js/autocomplete/autocomplete-list.js b/assets/js/autocomplete/autocomplete-list.js
index 8d9f5cf05..693c94715 100644
--- a/assets/js/autocomplete/autocomplete-list.js
+++ b/assets/js/autocomplete/autocomplete-list.js
@@ -1,6 +1,7 @@
import { getSuggestions } from './suggestions'
import { isBlank, qs } from '../helpers'
import { currentTheme } from '../theme'
+import autocompleteSuggestionsTemplate from '../handlebars/templates/autocomplete-suggestions.handlebars'
export const AUTOCOMPLETE_CONTAINER_SELECTOR = '.autocomplete'
export const AUTOCOMPLETE_SUGGESTION_LIST_SELECTOR = '.autocomplete-suggestions'
@@ -56,7 +57,7 @@ export function updateAutocompleteList (searchTerm) {
// Updates list of suggestions inside the autocomplete.
function renderSuggestions ({ term, suggestions }) {
- const autocompleteContainerHtml = Handlebars.templates['autocomplete-suggestions']({ suggestions, term })
+ const autocompleteContainerHtml = autocompleteSuggestionsTemplate({ suggestions, term })
const autocompleteContainer = qs(AUTOCOMPLETE_CONTAINER_SELECTOR)
autocompleteContainer.innerHTML = autocompleteContainerHtml
diff --git a/assets/js/handlebars/helpers.js b/assets/js/handlebars/helpers.js
index 6452cb49b..17c355e4b 100644
--- a/assets/js/handlebars/helpers.js
+++ b/assets/js/handlebars/helpers.js
@@ -1,3 +1,5 @@
+import * as Handlebars from 'handlebars/runtime'
+
Handlebars.registerHelper('groupChanged', function (context, nodeGroup, options) {
const group = nodeGroup || ''
if (context.group !== group) {
diff --git a/assets/js/modal.js b/assets/js/modal.js
index 22b314d26..1bc62dc29 100644
--- a/assets/js/modal.js
+++ b/assets/js/modal.js
@@ -1,4 +1,5 @@
import { qs } from './helpers'
+import modalLayoutTemplate from './handlebars/templates/modal-layout.handlebars'
const MODAL_SELECTOR = '.modal'
const MODAL_CLOSE_BUTTON_SELECTOR = '.modal .modal-close'
@@ -22,7 +23,7 @@ export function initialize () {
* Adds the modal to DOM, initially it's hidden.
*/
function renderModal () {
- const modalLayoutHtml = Handlebars.templates['modal-layout']()
+ const modalLayoutHtml = modalLayoutTemplate()
document.body.insertAdjacentHTML('beforeend', modalLayoutHtml)
qs(MODAL_SELECTOR).addEventListener('keydown', event => {
diff --git a/assets/js/quick-switch.js b/assets/js/quick-switch.js
index a59ff028f..8871c8e14 100644
--- a/assets/js/quick-switch.js
+++ b/assets/js/quick-switch.js
@@ -1,5 +1,7 @@
import { debounce, qs, qsAll } from './helpers'
import { openModal } from './modal'
+import quickSwitchModalBodyTemplate from './handlebars/templates/quick-switch-modal-body.handlebars'
+import quickSwitchResultsTemplate from './handlebars/templates/quick-switch-results.handlebars'
const HEX_DOCS_ENDPOINT = 'https://hexdocs.pm/%%'
const OTP_DOCS_ENDPOINT = 'https://www.erlang.org/doc/apps/%%'
@@ -115,7 +117,7 @@ function handleInput (event) {
export function openQuickSwitchModal () {
openModal({
title: 'Go to package docs',
- body: Handlebars.templates['quick-switch-modal-body']()
+ body: quickSwitchModalBodyTemplate()
})
qs(QUICK_SWITCH_INPUT_SELECTOR).focus()
@@ -185,7 +187,7 @@ function queryForAutocomplete (packageSlug) {
function renderResults ({ results }) {
const resultsContainer = qs(QUICK_SWITCH_RESULTS_SELECTOR)
- const resultsHtml = Handlebars.templates['quick-switch-results']({ results })
+ const resultsHtml = quickSwitchResultsTemplate({ results })
resultsContainer.innerHTML = resultsHtml
qsAll(QUICK_SWITCH_RESULT_SELECTOR).forEach(result => {
diff --git a/assets/js/search-page.js b/assets/js/search-page.js
index aa91066b8..17ee6d7ee 100644
--- a/assets/js/search-page.js
+++ b/assets/js/search-page.js
@@ -3,6 +3,7 @@
import lunr from 'lunr'
import { qs, escapeHtmlEntities, isBlank, getQueryParamByName, getProjectNameAndVersion } from './helpers'
import { setSearchInputValue } from './search-bar'
+import searchResultsTemplate from './handlebars/templates/search-results.handlebars'
const EXCERPT_RADIUS = 80
const SEARCH_CONTAINER_SELECTOR = '#search'
@@ -48,7 +49,7 @@ async function search (value) {
function renderResults ({ value, results, errorMessage }) {
const searchContainer = qs(SEARCH_CONTAINER_SELECTOR)
- const resultsHtml = Handlebars.templates['search-results']({ value, results, errorMessage })
+ const resultsHtml = searchResultsTemplate({ value, results, errorMessage })
searchContainer.innerHTML = resultsHtml
}
diff --git a/assets/js/settings.js b/assets/js/settings.js
index 365e3c12e..ad383134c 100644
--- a/assets/js/settings.js
+++ b/assets/js/settings.js
@@ -2,6 +2,7 @@ import { qs, qsAll } from './helpers'
import { openModal } from './modal'
import { settingsStore } from './settings-store'
import { keyboardShortcuts } from './keyboard-shortcuts'
+import settingsModalBodyTemplate from './handlebars/templates/settings-modal-body.handlebars'
const SETTINGS_LINK_SELECTOR = '.display-settings'
const SETTINGS_MODAL_BODY_SELECTOR = '#settings-modal-content'
@@ -53,7 +54,7 @@ function showKeyboardShortcutsTab () {
export function openSettingsModal () {
openModal({
title: modalTabs.map(({id, title}) => ``).join(''),
- body: Handlebars.templates['settings-modal-body']({ shortcuts: keyboardShortcuts })
+ body: settingsModalBodyTemplate({ shortcuts: keyboardShortcuts })
})
const modal = qs(SETTINGS_MODAL_BODY_SELECTOR)
diff --git a/assets/js/sidebar/sidebar-list.js b/assets/js/sidebar/sidebar-list.js
index 86720a5d8..397cf5a57 100644
--- a/assets/js/sidebar/sidebar-list.js
+++ b/assets/js/sidebar/sidebar-list.js
@@ -1,5 +1,6 @@
import { qs, getCurrentPageSidebarType, getLocationHash, findSidebarCategory } from '../helpers'
import { getSidebarNodes } from '../globals'
+import sidebarItemsTemplate from '../handlebars/templates/sidebar-items.handlebars'
const SIDEBAR_TYPE = {
search: 'search',
@@ -42,7 +43,7 @@ function renderSidebarNodeList (nodesByType, type) {
// Render the list
const nodeList = qs(sidebarNodeListSelector(type))
if (!nodeList) { return }
- const listContentHtml = Handlebars.templates['sidebar-items']({ nodes, group: '' })
+ const listContentHtml = sidebarItemsTemplate({ nodes, group: '' })
nodeList.innerHTML = listContentHtml
// Removes the "expand" class from links belonging to single-level sections
diff --git a/assets/js/sidebar/sidebar-version-select.js b/assets/js/sidebar/sidebar-version-select.js
index 9b3d48e88..f10ade61e 100644
--- a/assets/js/sidebar/sidebar-version-select.js
+++ b/assets/js/sidebar/sidebar-version-select.js
@@ -1,5 +1,6 @@
import { qs, checkUrlExists } from '../helpers'
import { getVersionNodes } from '../globals'
+import versionsDropdownTemplate from '../handlebars/templates/versions-dropdown.handlebars'
const VERSIONS_CONTAINER_SELECTOR = '.sidebar-projectVersion'
const VERSIONS_DROPDOWN_SELECTOR = '.sidebar-projectVersionsDropdown'
@@ -22,7 +23,7 @@ export function initialize () {
function renderVersionsDropdown ({ nodes }) {
const versionsContainer = qs(VERSIONS_CONTAINER_SELECTOR)
- const versionsDropdownHtml = Handlebars.templates['versions-dropdown']({ nodes })
+ const versionsDropdownHtml = versionsDropdownTemplate({ nodes })
versionsContainer.innerHTML = versionsDropdownHtml
qs(VERSIONS_DROPDOWN_SELECTOR).addEventListener('change', handleVersionSelected)
diff --git a/assets/js/tabsets.js b/assets/js/tabsets.js
index 8c33c51a3..251ed2023 100644
--- a/assets/js/tabsets.js
+++ b/assets/js/tabsets.js
@@ -1,3 +1,5 @@
+import tabsetTemplate from './handlebars/templates/tabset.handlebars'
+
const CONTENT_CONTAINER_ID = 'content'
const TABSET_OPEN_COMMENT = 'tabs-open'
const TABSET_CLOSE_COMMENT = 'tabs-close'
@@ -78,7 +80,7 @@ function processTabset (element, tabSetIndex, _array) {
wrapElements(allSetNodes, container)
// Apply template to tabset container element.
- container.innerHTML = Handlebars.templates.tabset({tabs: tabSet})
+ container.innerHTML = tabsetTemplate({tabs: tabSet})
// Return tabset container element.
return container
diff --git a/assets/js/tooltips/tooltips.js b/assets/js/tooltips/tooltips.js
index f7a38d82f..a220c62bc 100644
--- a/assets/js/tooltips/tooltips.js
+++ b/assets/js/tooltips/tooltips.js
@@ -1,6 +1,8 @@
import { qs, qsAll } from '../helpers'
import { settingsStore } from '../settings-store'
import { cancelHintFetchingIfAny, getHint, HINT_KIND, isValidHintHref } from './hints'
+import tooltipLayoutTemplate from '../handlebars/templates/tooltip-layout.handlebars'
+import tooltipBodyTemplate from '../handlebars/templates/tooltip-body.handlebars'
// Elements that can activate the tooltip.
const TOOLTIP_ACTIVATORS_SELECTOR = '.content a'
@@ -41,7 +43,7 @@ export function initialize () {
}
function renderTooltipLayout () {
- const tooltipLayoutHtml = Handlebars.templates['tooltip-layout']()
+ const tooltipLayoutHtml = tooltipLayoutTemplate()
qs(CONTENT_INNER_SELECTOR).insertAdjacentHTML('beforeend', tooltipLayoutHtml)
}
@@ -108,7 +110,7 @@ function shouldShowTooltips () {
}
function renderTooltip (hint) {
- const tooltipBodyHtml = Handlebars.templates['tooltip-body']({
+ const tooltipBodyHtml = tooltipBodyTemplate({
isPlain: hint.kind === HINT_KIND.plain,
hint
})
diff --git a/assets/package.json b/assets/package.json
index a41c67550..82b64e553 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -9,9 +9,9 @@
"npm": ">= 9.0.0"
},
"scripts": {
- "lint": "npx eslint './js/**/*.js'",
- "lint:fix": "npx eslint --fix './js/**/*.js'",
- "test": "npx karma start ./karma.conf.js --single-run",
+ "lint": "eslint './js/**/*.js'",
+ "lint:fix": "eslint --fix './js/**/*.js'",
+ "test": "karma start ./karma.conf.js --single-run",
"build:watch": "npm run build --watch",
"build": "node build/build.js"
},
diff --git a/lib/ex_doc/formatter/html/templates/head_template.eex b/lib/ex_doc/formatter/html/templates/head_template.eex
index 48e8e9be0..b8aa6414e 100644
--- a/lib/ex_doc/formatter/html/templates/head_template.eex
+++ b/lib/ex_doc/formatter/html/templates/head_template.eex
@@ -17,8 +17,6 @@
<%= if config.canonical do %>
<% end %>
-
-
diff --git a/mix.exs b/mix.exs
index 2c3fcf449..7112a3b9a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -56,7 +56,7 @@ defmodule ExDoc.Mixfile do
clean: [&clean_test_fixtures/1, "clean"],
fix: ["format", "cmd --cd assets npm run lint:fix"],
lint: ["format --check-formatted", "cmd --cd assets npm run lint"],
- setup: ["deps.get", "cmd mkdir -p tmp/handlebars", "cmd --cd assets npm install"]
+ setup: ["deps.get", "cmd --cd assets npm install"]
]
end