diff --git a/config.js b/config.js index 9bc8aa82..d70274ae 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,6 @@ export default { url: 'https://www.accessibility-developer-guide.com', + repoUrl: 'https://github.com/Access4all/adg', title: 'Accessibility Developer Guide', description: '', // TODO: Add twitter: '', // TODO: Add diff --git a/config/recent-pages.js b/config/recent-pages.js new file mode 100644 index 00000000..ba7c7c45 --- /dev/null +++ b/config/recent-pages.js @@ -0,0 +1,37 @@ +export default [ + { + commit: '23924f765de3b11b4c09bdc6ea6459a166b75317', + pages: [ + 'examples/hiding-elements/skip-navigation-links', + 'knowledge/keyboard-only/skip-navigation-links' + ] + }, + { + commit: '92cf70a701b23d2dcedb74a8521de7e8cac761b0', + pages: ['introduction/about'] + }, + { + commit: '703b03c2ef4581b27f743e3136d626a3e36edd58', + pages: ['examples/widgets/accordion'] + }, + { + commit: '517667bf917d002ea45092af00df472130d23f90', + pages: ['setup/windows/vmware-on-macos'] + }, + { + commit: '27b9f04175342243fb7a0d557a1867aa4038be09', + pages: ['examples/widgets/tablists'] + }, + { + commit: '9bd1f018edeb81e9d9e49e87863a3f161e92f0ff', + pages: ['knowledge/legal/eaa'] + }, + { + commit: '3ffb178c4c4388af0082249d4e0c8cf5bbdbea0a', + pages: ['examples/tables/spanning-rows-cols'] + }, + { + commit: '62db125e30d39aa7248c141561699b979738ffc9', + pages: ['knowledge/screen-readers/mobile/reading-websites'] + } +] diff --git a/gulp/helpers/datetime.js b/gulp/helpers/datetime.js index 37e0aa8a..65a8c73c 100644 --- a/gulp/helpers/datetime.js +++ b/gulp/helpers/datetime.js @@ -3,24 +3,28 @@ * @param {string} date in form of '2018-07-01' * @returns {string} '1. July, 2018' */ -const formatDate = date => { - var monthNames = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ] +const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +] +const formatDate = date => { const dateTime = new Date(date) + if (Number.isNaN(dateTime.getTime())) { + return '' + } + const day = dateTime.getDate() const monthIndex = dateTime.getMonth() const year = dateTime.getFullYear() @@ -28,4 +32,24 @@ const formatDate = date => { return `${monthNames[monthIndex]} ${day}, ${year}` } -export { formatDate } +const formatDateTime = timestamp => { + const dateTime = new Date( + typeof timestamp === 'number' && timestamp < 1_000_000_000_000 + ? timestamp * 1000 + : timestamp + ) + + if (Number.isNaN(dateTime.getTime())) { + return '' + } + + const day = dateTime.getDate() + const monthIndex = dateTime.getMonth() + const year = dateTime.getFullYear() + const hours = String(dateTime.getHours()).padStart(2, '0') + const minutes = String(dateTime.getMinutes()).padStart(2, '0') + + return `${monthNames[monthIndex]} ${day}, ${year}, ${hours}:${minutes}` +} + +export { formatDate, formatDateTime } diff --git a/gulp/helpers/git-metadata.js b/gulp/helpers/git-metadata.js new file mode 100644 index 00000000..3d67558d --- /dev/null +++ b/gulp/helpers/git-metadata.js @@ -0,0 +1,159 @@ +import childProcess from 'node:child_process' +import crypto from 'node:crypto' + +const gitHistoryRef = (() => { + for (const ref of ['main', 'master', 'HEAD']) { + const result = childProcess.spawnSync( + 'git', + ['rev-parse', '--verify', ref], + { + encoding: 'utf8' + } + ) + + if (result.status === 0) { + return result.stdout.trim() + } + } + + return 'HEAD' +})() +const gravatarImageSize = 48 + +const getGravatarUrl = email => { + if (!email) { + return '' + } + + const normalizedEmail = String(email).trim().toLowerCase() + + if (!normalizedEmail) { + return '' + } + + const hash = crypto.createHash('sha256').update(normalizedEmail).digest('hex') + + return `https://gravatar.com/avatar/${hash}?s=${gravatarImageSize}&d=mp` +} + +// Persist across watch rebuilds; git history does not change on every markdown save. +const changedMetadata = {} +const fileMergeHistoryCache = {} +const fileChangeStatsCache = new Map() + +export default () => { + const parseGitHistory = historyStdout => + historyStdout + .split('\x1e') + .map(item => item.trim()) + .filter(Boolean) + .map(item => { + const [ + commitId = '', + changed = '', + changedTimestamp = '', + changedBy = '', + changedByEmail = '', + commitMessage = '' + ] = item.split('\x1f') + + return { + commitId, + changed, + changedTimestamp: Number(changedTimestamp), + changedBy, + changedByEmail, + commitMessage, + gravatarUrl: getGravatarUrl(changedByEmail) + } + }) + .filter(entry => entry.commitId) + + const getFileMergeHistory = filePath => { + if (fileMergeHistoryCache[filePath]) { + return fileMergeHistoryCache[filePath] + } + + const historyStdout = childProcess.spawnSync( + 'git', + [ + 'log', + gitHistoryRef, + '--pretty=format:%H%x1f%ci%x1f%ct%x1f%an%x1f%ae%x1f%s%x1e', + '-m', + '--merges', + '--first-parent', + '--', + filePath + ], + { encoding: 'utf8' } + ).stdout + + const history = parseGitHistory(historyStdout) + + fileMergeHistoryCache[filePath] = history + return history + } + + const getFileChangeStats = (commitId, filePath) => { + if (!commitId || !filePath) { + return { linesAdded: 0, linesDeleted: 0 } + } + + const cacheKey = `${commitId}\x1f${filePath}` + + if (fileChangeStatsCache.has(cacheKey)) { + return fileChangeStatsCache.get(cacheKey) + } + + const numstatStdout = childProcess.spawnSync( + 'git', + [ + 'show', + '-m', + '--first-parent', + '--numstat', + '--format=tformat:', + commitId, + '--', + filePath + ], + { encoding: 'utf8' } + ).stdout + + const [numstatLine = ''] = numstatStdout + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + const [added = '0', deleted = '0'] = numstatLine.split('\t') + const stats = { + linesAdded: added === '-' ? 0 : Number(added) || 0, + linesDeleted: deleted === '-' ? 0 : Number(deleted) || 0 + } + + fileChangeStatsCache.set(cacheKey, stats) + return stats + } + + const getGitMetadata = filePath => { + if (changedMetadata[filePath]) { + return changedMetadata[filePath] + } + + const latestEntry = getFileMergeHistory(filePath)[0] + + const metadata = { + changed: latestEntry ? latestEntry.changed : '', + changedTimestamp: latestEntry ? latestEntry.changedTimestamp : 0, + changedBy: latestEntry ? latestEntry.changedBy : '', + gravatarUrl: latestEntry ? latestEntry.gravatarUrl : '', + commitId: latestEntry ? latestEntry.commitId : '', + commitMessage: latestEntry ? latestEntry.commitMessage : '' + } + + changedMetadata[filePath] = metadata + return metadata + } + + return { getGitMetadata, getFileChangeStats, getFileMergeHistory } +} diff --git a/gulp/helpers/page-frontmatter.js b/gulp/helpers/page-frontmatter.js new file mode 100644 index 00000000..dc5d3bb9 --- /dev/null +++ b/gulp/helpers/page-frontmatter.js @@ -0,0 +1,65 @@ +import fs from 'node:fs' +import path from 'node:path' +import frontMatter from 'front-matter' + +const pagesDirectory = './pages' + +const collectPageMarkdownFiles = (directory, markdownFiles = []) => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + if (entry.name === '_examples') { + continue + } + + collectPageMarkdownFiles(entryPath, markdownFiles) + continue + } + + if (entry.name.endsWith('.md')) { + markdownFiles.push(entryPath) + } + } + + return markdownFiles +} + +const getPageDirectory = (filePath, pagesRoot) => { + const relativePath = path.relative(pagesRoot, filePath) + const relativeDirectory = path.dirname(relativePath) + + return relativeDirectory === '.' ? '' : relativeDirectory.replace(/\\/g, '/') +} + +const getPagesWithFrontMatterFlag = (rootDir, flag) => { + const pagesRoot = path.join(rootDir, pagesDirectory) + + return collectPageMarkdownFiles(pagesRoot) + .map(filePath => { + const { attributes } = frontMatter(fs.readFileSync(filePath, 'utf8')) + + if (!attributes[flag]) { + return null + } + + return { + filePath, + directory: getPageDirectory(filePath, pagesRoot) + } + }) + .filter(Boolean) +} + +const getDevOnlyPageGlobs = rootDir => + getPagesWithFrontMatterFlag(rootDir, 'dev_only').map( + ({ filePath, directory }) => + `!./pages/${directory || path.basename(filePath, path.extname(filePath))}/**/*.md` + ) + +const getDevOnlyDistPaths = rootDir => + getPagesWithFrontMatterFlag(rootDir, 'dev_only').map( + ({ directory }) => `./dist/${directory}` + ) + +export { getDevOnlyDistPaths, getDevOnlyPageGlobs } diff --git a/gulp/html.js b/gulp/html.js index b1394ccc..30025244 100644 --- a/gulp/html.js +++ b/gulp/html.js @@ -1,7 +1,7 @@ -import child_process from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import { Readable } from 'node:stream' +import { deleteAsync } from 'del' import gulp from 'gulp' import handlebars from 'gulp-hb' import frontMatter from 'gulp-front-matter' @@ -11,10 +11,13 @@ import normalize from 'normalize-strings' import { SitemapStream, streamToPromise } from 'sitemap' import { JSDOM } from 'jsdom' import appConfig from '../config.js' -import { formatDate } from './helpers/datetime.js' +import recentPagesConfig from '../config/recent-pages.js' +import { formatDate, formatDateTime } from './helpers/datetime.js' import markdownFactory from './helpers/markdown.js' import { generateTags } from './helpers/metatags.js' import Feed from './helpers/rss.js' +import getGitMetadataFactory from './helpers/git-metadata.js' +import { getDevOnlyDistPaths } from './helpers/page-frontmatter.js' const pathSeparatorRegExp = new RegExp('\\' + path.sep, 'g') @@ -26,6 +29,15 @@ const getUrl = (filePath, base) => { .replace(/\/$/, '') } +const getCurrentUrl = (filePath, base) => { + const relPath = path.relative(base, filePath) + const lastSeparatorIndex = relPath.lastIndexOf(path.sep) + + return ( + lastSeparatorIndex >= 0 ? relPath.substring(0, lastSeparatorIndex) : '' + ).replace(pathSeparatorRegExp, '/') +} + const getLayout = (layoutName, layouts) => { layoutName = layoutName || 'layout' @@ -133,16 +145,359 @@ const flattenNavigation = items => return acc }, []) -// Cache changed dates -const changedDates = {} +const normalizeRecentPagesConfig = recentPages => { + if (!Array.isArray(recentPages)) { + return { urlOnly: [], commits: [] } + } + + const urlOnly = [] + const commits = [] + + for (const entry of recentPages) { + if (typeof entry === 'string') { + const url = entry.replace(/^\/+/, '').trim() + + if (url) { + urlOnly.push(url) + } + + continue + } + + if (!entry || typeof entry !== 'object') { + continue + } + + const commit = typeof entry.commit === 'string' ? entry.commit.trim() : '' + const pages = Array.isArray(entry.pages) + ? [ + ...new Set( + entry.pages + .filter(page => typeof page === 'string') + .map(page => page.replace(/^\/+/, '').trim()) + .filter(Boolean) + ) + ] + : [] + + if (commit && pages.length) { + commits.push({ commit, pages }) + } + } + + return { + urlOnly: [...new Set(urlOnly)], + commits + } +} + +const findMergeByCommit = (merges, commitRef) => { + if (!commitRef) { + return null + } + + return ( + merges.find( + merge => + merge.commitId === commitRef || merge.commitId.startsWith(commitRef) + ) || null + ) +} + +const applyMergeMetadata = (page, merge) => ({ + ...page, + changed: merge.changed, + changedTimestamp: merge.changedTimestamp, + changedBy: merge.changedBy, + gravatarUrl: merge.gravatarUrl, + commitId: merge.commitId, + commitMessage: merge.commitMessage, + commitUrl: merge.commitUrl +}) + +const isGuideNavigationPage = file => + !file.frontMatter.navigation_ignore && + !file.frontMatter.page_updates_overview && + !file.frontMatter.all_pages_overview + +const getRecentlyUpdatedPages = (entries, recentPages, mergeOptions) => { + const { urlOnly, commits } = normalizeRecentPagesConfig(recentPages) + + if (!urlOnly.length && !commits.length) { + return entries + } + + if (commits.length) { + const entryByUrl = new Map(entries.map(entry => [entry.url, entry])) + const merges = groupPageEntriesByMerge(entries, mergeOptions) + const results = [] + + for (const item of commits) { + const merge = findMergeByCommit(merges, item.commit) + + for (const url of item.pages) { + const base = entryByUrl.get(url) + + if (!base) { + continue + } + + results.push(merge ? applyMergeMetadata(base, merge) : base) + } + } + + return results + } + + const selectedUrlSet = new Set(urlOnly) + + return entries.filter(page => selectedUrlSet.has(page.url)) +} + +const markMergePageSelection = (merges, recentPages) => { + const { commits } = normalizeRecentPagesConfig(recentPages) + const selectedByCommit = new Map() + + for (const item of commits) { + const merge = findMergeByCommit(merges, item.commit) + + if (merge) { + selectedByCommit.set(merge.commitId, new Set(item.pages)) + } + } + + return merges.map(merge => ({ + ...merge, + pages: merge.pages.map(page => ({ + ...page, + isSelected: selectedByCommit.get(merge.commitId)?.has(page.url) ?? false + })) + })) +} + +let devAllPagesListCache = null + +const MIN_MEANINGFUL_REVISION_LINES = 4 + +const isMeaningfulRevision = (linesAdded, linesDeleted) => + linesAdded >= MIN_MEANINGFUL_REVISION_LINES || + linesDeleted >= MIN_MEANINGFUL_REVISION_LINES + +const getMeaningfulPageRevision = (filePath, mergeOptions) => { + const { getFileMergeHistory, getFileChangeStats } = mergeOptions + + for (const mergeEntry of getFileMergeHistory(filePath)) { + const { linesAdded, linesDeleted } = getFileChangeStats( + mergeEntry.commitId, + filePath + ) + const linesChanged = linesAdded + linesDeleted + + if (isMeaningfulRevision(linesAdded, linesDeleted)) { + return { + changed: mergeEntry.changed, + changedTimestamp: mergeEntry.changedTimestamp, + changedBy: mergeEntry.changedBy, + gravatarUrl: mergeEntry.gravatarUrl, + commitId: mergeEntry.commitId, + commitShortId: mergeEntry.commitId.slice(0, 7), + commitMessage: mergeEntry.commitMessage, + linesAdded, + linesDeleted, + linesChanged + } + } + } + + return null +} + +const getAllPagesByRevision = ( + pageFiles, + navigationItems, + mergeOptions, + base +) => + pageFiles + .map(file => { + const url = getCurrentUrl(file.path, base) + const section = file.data.section + const sectionTitle = + navigationItems.find(item => item.url === section)?.title || '' + const revision = getMeaningfulPageRevision(file.path, mergeOptions) + + return { + title: file.data.title, + url, + section, + sectionTitle, + changed: revision?.changed || '', + changedTimestamp: revision?.changedTimestamp || 0, + changedBy: revision?.changedBy || '', + gravatarUrl: revision?.gravatarUrl || '', + commitId: revision?.commitId || '', + commitShortId: revision?.commitShortId || '', + commitMessage: revision?.commitMessage || '', + linesAdded: revision?.linesAdded || 0, + linesDeleted: revision?.linesDeleted || 0, + linesChanged: revision?.linesChanged || 0 + } + }) + .filter(page => page.title && page.url) + .sort((a, b) => { + if (b.changedTimestamp !== a.changedTimestamp) { + return b.changedTimestamp - a.changedTimestamp + } + + return a.title.localeCompare(b.title) + }) + +const groupPageEntriesByMerge = (pages, mergeOptions = {}) => { + const { + getFileMergeHistory, + getFileChangeStats, + githubRepoUrl = '' + } = mergeOptions + const mergesByCommitId = new Map() + + for (const page of pages) { + if (!page.filePath || !getFileMergeHistory || !getFileChangeStats) { + continue + } + + for (const mergeEntry of getFileMergeHistory(page.filePath)) { + const { linesAdded, linesDeleted } = getFileChangeStats( + mergeEntry.commitId, + page.filePath + ) + + if (!isMeaningfulRevision(linesAdded, linesDeleted)) { + continue + } + + const affectedPage = { + title: page.title, + url: page.url, + section: page.section, + sectionTitle: page.sectionTitle, + linesAdded, + linesDeleted, + linesChanged: linesAdded + linesDeleted + } + const existingMerge = mergesByCommitId.get(mergeEntry.commitId) + + if (existingMerge) { + if (existingMerge.pages.some(entry => entry.url === page.url)) { + continue + } + + existingMerge.pages.push(affectedPage) + continue + } + + mergesByCommitId.set(mergeEntry.commitId, { + changed: mergeEntry.changed, + changedTimestamp: mergeEntry.changedTimestamp, + changedBy: mergeEntry.changedBy, + gravatarUrl: mergeEntry.gravatarUrl, + commitId: mergeEntry.commitId, + commitShortId: mergeEntry.commitId.slice(0, 7), + commitMessage: mergeEntry.commitMessage, + commitUrl: githubRepoUrl + ? `${githubRepoUrl}/commit/${mergeEntry.commitId}` + : '', + pages: [affectedPage] + }) + } + } + + return Array.from(mergesByCommitId.values()) + .map(merge => ({ + ...merge, + pages: merge.pages.sort((a, b) => { + if (b.linesChanged !== a.linesChanged) { + return b.linesChanged - a.linesChanged + } + + return a.title.localeCompare(b.title) + }) + })) + .sort((a, b) => b.changedTimestamp - a.changedTimestamp) +} export default (config, cb) => { + buildHtml(config, cb) +} + +const buildHtml = (config, cb) => { + const devMode = Boolean(config.devMode) const markdown = markdownFactory(config.rootDir) + const { getGitMetadata, getFileChangeStats, getFileMergeHistory } = + getGitMetadataFactory() + const githubRepoUrl = appConfig.repoUrl + const mergeOptions = { + getFileMergeHistory, + getFileChangeStats, + githubRepoUrl + } + const getUpdatedPageEntries = currentFilePath => + files + .filter(isGuideNavigationPage) + .map(file => { + const metadata = getGitMetadata(file.path) + const { linesAdded, linesDeleted } = metadata.commitId + ? getFileChangeStats(metadata.commitId, file.path) + : { linesAdded: 0, linesDeleted: 0 } + const section = file.data.section + const sectionTitle = + navigation.find(item => item.url === section)?.title || '' + + return { + title: file.data.title, + lead: file.data.lead, + url: getCurrentUrl(file.path, config.base), + filePath: file.path, + section, + sectionTitle, + linesAdded, + linesDeleted, + linesChanged: linesAdded + linesDeleted, + changed: metadata.changed, + changedTimestamp: metadata.changedTimestamp, + changedBy: metadata.changedBy, + gravatarUrl: metadata.gravatarUrl, + commitId: metadata.commitId, + commitMessage: metadata.commitMessage, + commitUrl: metadata.commitId + ? `${githubRepoUrl}/commit/${metadata.commitId}` + : '' + } + }) + .filter( + page => + page.title && + page.url && + page.changed && + page.url !== getCurrentUrl(currentFilePath, config.base) + ) + .sort((a, b) => b.changedTimestamp - a.changedTimestamp) const files = [] const sitemap = [] const layouts = {} let navigation = [] + let updatedPageEntries = null + let pageUpdatesOverviewFile = null + let homeFile = null + + const ensureUpdatedPageEntries = currentFilePath => { + if (updatedPageEntries !== null) { + return updatedPageEntries + } + + updatedPageEntries = getUpdatedPageEntries(currentFilePath) + return updatedPageEntries + } // const config = { // src: './pages/**/*.md', @@ -223,6 +578,12 @@ export default (config, cb) => { .filter(page => !page.parent && page.parent !== null) .sort((a, b) => a.position - b.position) + homeFile = files.find( + file => getCurrentUrl(file.path, config.base) === '' + ) + pageUpdatesOverviewFile = + files.find(file => file.frontMatter.page_updates_overview) || null + // Return files back to stream files.forEach(this.push.bind(this)) @@ -238,9 +599,7 @@ export default (config, cb) => { try { const layout = getLayout(file.frontMatter.layout, layouts) const relPath = path.relative('./pages', file.path) - const currentUrl = relPath - .substring(0, relPath.lastIndexOf(path.sep)) - .replace(pathSeparatorRegExp, '/') + const currentUrl = getCurrentUrl(file.path, config.base) const prevNext = {} const breadcrumb = [] const subPages = [] @@ -259,19 +618,58 @@ export default (config, cb) => { site_name: appConfig.title, url: `${appConfig.url}/${currentUrl}` } - const dateChanged = - changedDates[file.path] || - child_process.spawnSync( - 'git', - ['log', '-1', '--pretty=format:%ci', file.path], - { encoding: 'utf8' } - ).stdout - - changedDates[file.path] = dateChanged + const metadata = devMode ? null : getGitMetadata(file.path) + const recentlyUpdatedPages = + currentUrl === '' + ? getRecentlyUpdatedPages( + ensureUpdatedPageEntries(homeFile?.path ?? file.path), + recentPagesConfig, + mergeOptions + ) + : [] + const pageUpdatesConfigurator = + devMode && file.frontMatter.page_updates_overview + const pageUpdatesList = pageUpdatesConfigurator + ? markMergePageSelection( + groupPageEntriesByMerge( + ensureUpdatedPageEntries(file.path), + mergeOptions + ), + recentPagesConfig + ) + : [] + const allPagesList = + devMode && file.frontMatter.all_pages_overview + ? devAllPagesListCache || + (devAllPagesListCache = getAllPagesByRevision( + files.filter(isGuideNavigationPage), + navigation, + mergeOptions, + config.base + )) + : [] + const recentPagesOverviewLink = + devMode && currentUrl === '' && pageUpdatesOverviewFile + ? { + title: pageUpdatesOverviewFile.data.title, + url: getCurrentUrl( + pageUpdatesOverviewFile.path, + config.base + ) + } + : null + + const pageChanged = + metadata?.changed && metadata.changed.length > 0 + ? metadata.changed + : null + const showPageMetaInfo = devMode + ? Boolean(currentUrl && file.data.section !== 'welcome') + : Boolean(pageChanged) file.data = Object.assign({}, file.data, { - changed: - dateChanged && dateChanged.length > 0 ? dateChanged : null, + changed: pageChanged, + showPageMetaInfo, title: file.data.title, contents: file.contents, navigation: pageNavigation, @@ -285,11 +683,16 @@ export default (config, cb) => { level: 1 })) : subPages, + recentlyUpdatedPages, + pageUpdatesList, + pageUpdatesConfigurator, + allPagesList, + recentPagesOverviewLink, metatags: generateTags(metatagsData), breadcrumb: breadcrumb.sort((a, b) => { return a.url.length - b.url.length }), - fileHistory: `https://github.com/Access4all/adg/commits/main/pages/${relPath}` + fileHistory: `${githubRepoUrl}/commits/main/pages/${relPath}` }) sitemap.push({ @@ -321,6 +724,21 @@ export default (config, cb) => { }, helpers: { formatDate, + formatDateTime, + truncateText: function (text, maxLength) { + if (!text) { + return '' + } + + const normalizedText = String(text).trim().replace(/\s+/g, ' ') + const limit = Number(maxLength) + + if (!Number.isFinite(limit) || normalizedText.length <= limit) { + return normalizedText + } + + return `${normalizedText.slice(0, limit).trimEnd()}...` + }, eq: function (v1, v2, options) { if (v1 === v2) { return options.fn(this) @@ -338,6 +756,7 @@ export default (config, cb) => { or: function () { return Array.prototype.slice.call(arguments, 0, -1).some(Boolean) }, + json: context => JSON.stringify(context).replace(/ { fs.writeFileSync(config.sitemap, xml) + if (!devMode) { + await deleteAsync(getDevOnlyDistPaths(config.rootDir), { force: true }) + } + cb() }) } diff --git a/gulpfile.js b/gulpfile.js index 070f2523..2dc9a59c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -13,6 +13,7 @@ import html from './gulp/html.js' import css from './gulp/css.js' import js from './gulp/javascript.js' import examples from './gulp/examples.js' +import { getDevOnlyPageGlobs } from './gulp/helpers/page-frontmatter.js' const browserSync = browserSyncFactory.create() @@ -24,27 +25,33 @@ function errorHandler(err) { ) } +const isDevMode = () => process.env.ADG_DEV === '1' + +const getHtmlConfig = () => ({ + src: [ + './pages/**/*.md', + '!./pages/**/_examples/**/*.md', + ...(isDevMode() ? [] : getDevOnlyPageGlobs(import.meta.dirname)) + ], + base: './pages', + host: 'https://www.accessibility-developer-guide.com', + sitemap: './dist/sitemap.xml', + feed: { + json: './dist/feed/feed.json', + atom: './dist/feed/atom.xml', + rss: './dist/feed/rss.xml' + }, + devMode: isDevMode(), + errorHandler, + rootDir: import.meta.dirname +}) + gulp.task('html', cb => - html( - { - src: ['./pages/**/*.md', '!./pages/**/_examples/**/*.md'], - base: './pages', - host: 'https://www.accessibility-developer-guide.com', - sitemap: './dist/sitemap.xml', - feed: { - json: './dist/feed/feed.json', - atom: './dist/feed/atom.xml', - rss: './dist/feed/rss.xml' - }, - errorHandler, - rootDir: import.meta.dirname - }, - () => { - browserSync.reload() + html(getHtmlConfig(), () => { + browserSync.reload() - cb() - } - ) + cb() + }) ) gulp.task('html:examples', cb => @@ -252,54 +259,62 @@ gulp.task('rebuild', gulp.series('clean', 'build')) gulp.task( 'default', - gulp.series('build', function serveAndWatch() { - browserSync.init({ - server: { - baseDir: './dist' - } - }) + gulp.series( + function enableDevMode(cb) { + process.env.ADG_DEV = '1' + cb() + }, + 'build', + function serveAndWatch() { + browserSync.init({ + server: { + baseDir: './dist' + } + }) - gulp.watch( - ['./src/assets/css/**/*.scss', './src/components/**/*.scss'], - gulp.series('css') - ) - gulp.watch(['./src/assets/js/**/*.js'], gulp.series('js')) - gulp.watch( - [ - './pages/**/*.md', - './src/templates/**/*.hbs', - './src/components/**/*.hbs', - './gulp/helpers/*', - // Example content which is embedded in HTML pages - './pages/**/_examples/**/*.html', - './pages/**/_examples/**/*.js', - './pages/**/_examples/**/*.css' - ], - gulp.series('html') - ) - gulp.watch(['./pages/**/_examples/**/*'], gulp.series('html:examples')) - gulp.watch( - [ - // demo - './pages/**/_examples/**/*.html', - '!./pages/**/_examples/**/index.html', - // content - './pages/{,**/}_media/**/*', - './pages/**/*.{png,jpg,mp3}', - // assets - './src/assets/img/**/*', - // static - './pages/{,**/}_static/**/*' - ], - gulp.series('media:copy') - ) - gulp.watch( - ['./pages/{,**/}_media/**/*', './pages/**/_examples/**/*.png'], - gulp.series('media:resize') - ) - gulp.watch( - ['./src/assets/img/icons/**/*.png', '!./src/assets/img/icons/*.png'], - gulp.series('sprite') - ) - }) + gulp.watch( + ['./src/assets/css/**/*.scss', './src/components/**/*.scss'], + gulp.series('css') + ) + gulp.watch(['./src/assets/js/**/*.js'], gulp.series('js')) + gulp.watch( + [ + './pages/**/*.md', + './src/templates/**/*.hbs', + './src/components/**/*.hbs', + './gulp/helpers/*', + './gulp/html.js', + // Example content which is embedded in HTML pages + './pages/**/_examples/**/*.html', + './pages/**/_examples/**/*.js', + './pages/**/_examples/**/*.css' + ], + gulp.series('html') + ) + gulp.watch(['./pages/**/_examples/**/*'], gulp.series('html:examples')) + gulp.watch( + [ + // demo + './pages/**/_examples/**/*.html', + '!./pages/**/_examples/**/index.html', + // content + './pages/{,**/}_media/**/*', + './pages/**/*.{png,jpg,mp3}', + // assets + './src/assets/img/**/*', + // static + './pages/{,**/}_static/**/*' + ], + gulp.series('media:copy') + ) + gulp.watch( + ['./pages/{,**/}_media/**/*', './pages/**/_examples/**/*.png'], + gulp.series('media:resize') + ) + gulp.watch( + ['./src/assets/img/icons/**/*.png', '!./src/assets/img/icons/*.png'], + gulp.series('sprite') + ) + } + ) ) diff --git a/package-lock.json b/package-lock.json index 4b48d6db..7066532e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2190,9 +2190,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2210,9 +2207,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2230,9 +2224,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2250,9 +2241,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2270,9 +2258,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2290,9 +2275,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2310,9 +2292,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2330,9 +2309,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2350,9 +2326,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2376,9 +2349,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2402,9 +2372,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2428,9 +2395,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2454,9 +2418,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2480,9 +2441,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2506,9 +2464,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2532,9 +2487,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2957,9 +2909,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2981,9 +2930,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3005,9 +2951,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3029,9 +2972,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3053,9 +2993,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3077,9 +3014,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3273,13 +3207,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~7.21.0" } }, "node_modules/@types/sax": { @@ -3314,9 +3248,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", "engines": { @@ -3433,9 +3367,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3450,9 +3381,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3467,9 +3395,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3484,9 +3409,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3501,9 +3423,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3518,9 +3437,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3535,9 +3451,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3552,9 +3465,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4436,9 +4346,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", - "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4533,9 +4443,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -6032,9 +5942,9 @@ } }, "node_modules/editorconfig/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -6052,9 +5962,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.352", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", - "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==", + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", "dev": true, "license": "ISC" }, @@ -6132,9 +6042,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6338,9 +6248,9 @@ } }, "node_modules/eslint-plugin-import-x/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -7017,9 +6927,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -9176,9 +9086,9 @@ } }, "node_modules/lint-staged": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.2.tgz", - "integrity": "sha512-Rbr6rdmbCn1fIDHBZpn0madg0hEkdlh+QwajnL3Qq0ZUq/icAJfLGj9BVBajAXi7657ZzKQ7kobGP9S5XOHYRw==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.4.tgz", + "integrity": "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==", "dev": true, "license": "MIT", "dependencies": { @@ -10050,9 +9960,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "dev": true, "license": "MIT" }, @@ -12018,9 +11928,9 @@ } }, "node_modules/sharp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -12087,9 +11997,9 @@ } }, "node_modules/sitemap/node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -12678,9 +12588,9 @@ } }, "node_modules/terser": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", - "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12697,9 +12607,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", "dev": true, "license": "MIT", "dependencies": { @@ -12719,12 +12629,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -13093,9 +13030,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "dev": true, "license": "MIT" }, @@ -14024,9 +13961,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 5b9fb134..168b33e6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "index.js", "type": "module", "scripts": { + "dev": "ADG_DEV=1 gulp --webpackWatch", "start": "gulp --webpackWatch", "build": "gulp build", "rebuild": "gulp rebuild", diff --git a/pages/develop/README.md b/pages/develop/README.md new file mode 100644 index 00000000..8fca9105 --- /dev/null +++ b/pages/develop/README.md @@ -0,0 +1,9 @@ +--- +navigation_title: "Develop" +position: 6 +dev_only: true +--- + +# Develop + +**Development-only tools for maintaining the guide, including merge-based recent changes and a revision overview for every guide page.** diff --git a/pages/develop/page-revisions/README.md b/pages/develop/page-revisions/README.md new file mode 100644 index 00000000..3237b8c9 --- /dev/null +++ b/pages/develop/page-revisions/README.md @@ -0,0 +1,10 @@ +--- +navigation_title: "Page revisions" +position: 2 +dev_only: true +all_pages_overview: true +--- + +# Page revisions + +**Development overview of all guide pages, sorted by the most recent merge.** \ No newline at end of file diff --git a/pages/develop/recent-changes/README.md b/pages/develop/recent-changes/README.md new file mode 100644 index 00000000..a3a3fb0d --- /dev/null +++ b/pages/develop/recent-changes/README.md @@ -0,0 +1,10 @@ +--- +navigation_title: "Recent changes" +position: 1 +dev_only: true +page_updates_overview: true +--- + +# Recent changes by merge + +**Overview of all merges to main and the guide pages changed in each merge, with pages sorted by number of changed lines.** diff --git a/pages/introduction/license/README.md b/pages/introduction/license/README.md index 84ab6c63..d5b0d431 100644 --- a/pages/introduction/license/README.md +++ b/pages/introduction/license/README.md @@ -1,5 +1,6 @@ --- navigation_title: "License" +navigation_ignore: true position: 4 --- diff --git a/pages/introduction/privacy-policy/README.md b/pages/introduction/privacy-policy/README.md index b9afa5ce..0b8b708b 100644 --- a/pages/introduction/privacy-policy/README.md +++ b/pages/introduction/privacy-policy/README.md @@ -1,5 +1,6 @@ --- navigation_title: "Privacy Policy" +navigation_ignore: true position: 5 --- diff --git a/src/assets/js/app/modules.js b/src/assets/js/app/modules.js index fc0304bf..195a1d4f 100644 --- a/src/assets/js/app/modules.js +++ b/src/assets/js/app/modules.js @@ -4,6 +4,7 @@ import Search from './modules/Search.js' import Anchor from './modules/content/Anchor.js' import MainNav from './modules/content/MainNav.js' import Panel from './modules/content/Panel.js' +import PageUpdatesConfigurator from './modules/content/PageUpdatesConfigurator.js' export default () => { // every module should at least implement two methods @@ -38,6 +39,12 @@ export default () => { ModuleManager.connect(Panel, elem) }) + contextTrigger.add('.js-page-updates-configurator', function () { + var elem = this + + ModuleManager.connect(PageUpdatesConfigurator, elem) + }) + contextTrigger.validate('body') // console.log('Selecting components took: ', new Date() - time, 'ms') diff --git a/src/assets/js/app/modules/content/PageUpdatesConfigurator.js b/src/assets/js/app/modules/content/PageUpdatesConfigurator.js new file mode 100644 index 00000000..24d1dc7a --- /dev/null +++ b/src/assets/js/app/modules/content/PageUpdatesConfigurator.js @@ -0,0 +1,174 @@ +const formatJsString = value => + `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` + +const buildRecentPagesSnippet = groups => { + if (!groups.length) { + return '' + } + + const lines = ['export default ['] + + groups.forEach((group, index) => { + lines.push(' {') + lines.push(` commit: ${formatJsString(group.commit)},`) + lines.push(' pages: [') + + group.pages.forEach((url, pageIndex) => { + const suffix = pageIndex < group.pages.length - 1 ? ',' : '' + lines.push(` ${formatJsString(url)}${suffix}`) + }) + + lines.push(' ]') + lines.push(` }${index < groups.length - 1 ? ',' : ''}`) + }) + + lines.push(']') + + return lines.join('\n') +} + +/** + * Page updates configurator + * + * @selector .js-page-updates-configurator + * @enabled true + */ +export default class PageUpdatesConfigurator { + constructor() { + PageUpdatesConfigurator.namespaceIndex += 1 + this.ns = `.PageUpdatesConfigurator${PageUpdatesConfigurator.namespaceIndex}` + this.el = null + this.abortController = null + this.checkboxes = [] + this.textarea = null + this.status = null + this.handleChange = this.handleChange.bind(this) + this.copySnippet = this.copySnippet.bind(this) + } + + init(element) { + this.el = element + this.abortController = new AbortController() + const { signal } = this.abortController + + this.checkboxes = Array.from( + element.querySelectorAll('.page-updates__page-checkbox') + ) + this.textarea = element.querySelector('.page-updates__textarea') + this.status = element.querySelector('.page-updates__status') + + element.addEventListener('change', this.handleChange, { signal }) + + const copyButton = element.querySelector('.page-updates__copy') + + if (copyButton) { + copyButton.addEventListener('click', this.copySnippet, { signal }) + } + + this.updateSnippet() + + return this + } + + destroy() { + this.abortController?.abort() + this.abortController = null + this.el = null + this.checkboxes = [] + this.textarea = null + this.status = null + } + + handleChange(event) { + if (event.target.matches('.page-updates__page-checkbox')) { + this.updateSnippet() + } + } + + getSelectedGroups() { + const groups = [] + const groupByCommit = new Map() + + for (const checkbox of this.checkboxes) { + if (!checkbox.checked) { + continue + } + + const { commit } = checkbox.dataset + const { value: url } = checkbox + + if (!commit || !url) { + continue + } + + if (!groupByCommit.has(commit)) { + groupByCommit.set(commit, []) + } + + groupByCommit.get(commit).push(url) + } + + for (const item of this.el.querySelectorAll('.page-updates__item')) { + const checkbox = item.querySelector('.page-updates__page-checkbox') + const commit = checkbox?.dataset.commit + + if (!commit || !groupByCommit.has(commit)) { + continue + } + + groups.push({ + commit, + pages: groupByCommit.get(commit).sort((a, b) => a.localeCompare(b)) + }) + } + + return groups + } + + updateSnippet() { + const snippet = buildRecentPagesSnippet(this.getSelectedGroups()) + + if (this.textarea) { + this.textarea.value = snippet + } + + if (!this.status) { + return + } + + if (!snippet) { + this.status.textContent = + 'No pages selected. Without a config in config/recent-pages.js, the home page uses the latest git updates.' + return + } + + this.status.textContent = '' + } + + async copySnippet() { + const snippet = this.textarea?.value || '' + + if (!snippet) { + if (this.status) { + this.status.textContent = 'Nothing to copy.' + } + + return + } + + try { + await navigator.clipboard.writeText(snippet) + + if (this.status) { + this.status.textContent = 'Copied to clipboard.' + } + } catch { + if (this.status) { + this.status.textContent = + 'Copy failed. Select and copy the snippet manually.' + } + } + } +} + +PageUpdatesConfigurator.namespaceIndex = 0 diff --git a/src/components/base/theme/_theme.scss b/src/components/base/theme/_theme.scss index 12b14a26..b40f990c 100644 --- a/src/components/base/theme/_theme.scss +++ b/src/components/base/theme/_theme.scss @@ -27,7 +27,8 @@ --theme-color-light: #{$theme-1-light}; } -.theme-contribution { +.theme-contribution, +.theme-develop { --theme-color-dark: #{$theme-4-dark}; --theme-color-medium: #{$c-gray}; --theme-color-light: #{$theme-4-light}; diff --git a/src/components/content/all-pages/_all-pages-table.scss b/src/components/content/all-pages/_all-pages-table.scss new file mode 100644 index 00000000..9d87fa29 --- /dev/null +++ b/src/components/content/all-pages/_all-pages-table.scss @@ -0,0 +1,77 @@ +.all-pages { + @include rem(margin-top, $gutter); +} + +.all-pages__intro { + margin: 0 0 1em; + max-width: 48em; +} + +.all-pages__table-wrap { + overflow-x: auto; +} + +.all-pages__table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + line-height: 1.5; +} + +.all-pages__table th, +.all-pages__table td { + padding: 0.75em; + border-bottom: 1px solid var(--theme-color-medium, $c-gray); + text-align: left; + vertical-align: top; +} + +.all-pages__table th { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.all-pages__date { + display: block; + margin: 0; + font-weight: 600; + white-space: nowrap; +} + +.all-pages__section { + display: inline-block; + margin: 0; + padding: 0.1em 0.5em; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + line-height: 1.3; + letter-spacing: 0.01em; + background-color: var(--theme-color-light); + color: var(--theme-color-dark); + white-space: nowrap; +} + +.all-pages__link { + display: inline-block; + min-width: 12em; +} + +.all-pages__changes { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.all-pages__author { + display: flex; +} + +.all-pages__empty, +.all-pages__empty-date, +.all-pages__empty-changes, +.all-pages__empty-author { + margin: 0; + color: var(--theme-color-dark); +} diff --git a/src/components/content/all-pages/all-pages-table.hbs b/src/components/content/all-pages/all-pages-table.hbs new file mode 100644 index 00000000..c399abc3 --- /dev/null +++ b/src/components/content/all-pages/all-pages-table.hbs @@ -0,0 +1,74 @@ +
+

+ All guide pages sorted by last meaningful revision +

+ +

+ Each row shows the latest merge that changed more than three lines in that page file. Pages without such a revision appear at the end. +

+ + {{#if pages}} +
+ + + + + + + + + + + + + {{#each pages}} + + + + + + + + {{/each}} + +
+ Guide pages sorted by last revision date +
Last revisionSectionPageChangesAuthor
+ {{#if changedTimestamp}} + + {{else}} + No substantial revision + {{/if}} + + {{#if sectionTitle}} + + Section: {{sectionTitle}} + + {{/if}} + + {{title}} + + {{#if linesChanged}} + + +{{linesAdded}} −{{linesDeleted}} + + {{else}} + + {{/if}} + + {{#if changedBy}} + {{> "content/author/author" name=changedBy gravatarUrl=gravatarUrl className="all-pages__author"}} + {{else}} + + {{/if}} +
+
+ {{else}} +

No pages were found.

+ {{/if}} +
diff --git a/src/components/content/author/_author.scss b/src/components/content/author/_author.scss new file mode 100644 index 00000000..a8cf7efd --- /dev/null +++ b/src/components/content/author/_author.scss @@ -0,0 +1,20 @@ +.author { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + font-size: 16px; +} + +.author__avatar { + display: block; + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid rgba(0, 0, 0, 0.08); + flex: 0 0 auto; +} + +.author__name { + min-width: 0; +} diff --git a/src/components/content/author/author.hbs b/src/components/content/author/author.hbs new file mode 100644 index 00000000..68e88682 --- /dev/null +++ b/src/components/content/author/author.hbs @@ -0,0 +1,13 @@ + + {{#if gravatarUrl}} + + {{/if}} + {{name}} + diff --git a/src/components/content/page-updates/_page-updates-table.scss b/src/components/content/page-updates/_page-updates-table.scss new file mode 100644 index 00000000..01c0b335 --- /dev/null +++ b/src/components/content/page-updates/_page-updates-table.scss @@ -0,0 +1,161 @@ +.page-updates { + @include rem(margin-top, $gutter); +} + +.page-updates__intro { + margin: 0 0 1em; + max-width: 48em; +} + +.page-updates__list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 1em; +} + +.page-updates__item { + margin: 0; +} + +.page-updates__merge { + padding: 1em; + border: 1px solid var(--theme-color-medium, $c-gray); + border-radius: 10px; + background: var(--theme-main-color-inverse, $c-white); + box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.08); +} + +.page-updates__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5em 0.75em; +} + +.page-updates__date { + margin: 0; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.page-updates__commit code { + font-size: 0.9em; +} + +.page-updates__message { + margin: 0.75em 0 0; + font-size: 16px; + line-height: 1.5; +} + +.page-updates__pages { + margin: 0.75em 0 0; + padding: 0; + list-style: none; + border-top: 1px solid var(--theme-color-medium, $c-gray); +} + +.page-updates__page { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.5em 0.75em; + padding: 0.5em 0; + border-bottom: 1px solid var(--theme-color-medium, $c-gray); + + font-size: 12px; + &:last-child { + border-bottom: 0; + padding-bottom: 0; + } +} + +.page-updates__page-label { + display: inline-flex; + margin: 0; + cursor: pointer; +} + +.page-updates__page-checkbox { + margin: 0; +} + +.page-updates__link { + flex: 1 1 auto; + min-width: 12em; +} + +.page-updates__changes { + margin-left: auto; + font-size: 0.85em; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.page-updates__section { + display: inline-block; + margin: 0; + padding: 0.1em 0.5em; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + line-height: 1.3; + letter-spacing: 0.01em; + background-color: var(--theme-color-light); + color: var(--theme-color-dark); +} + +.page-updates__author { + display: flex; +} + +.page-updates__empty { + margin: 0; +} + +.page-updates__output { + margin-top: 1em; + padding-top: 1em; + border-top: 1px solid var(--theme-color-medium, $c-gray); +} + +.page-updates__output-label { + display: block; + margin-bottom: 0.5em; + font-weight: 600; +} + +.page-updates__textarea { + width: 100%; + min-height: 8em; + padding: 0.75em; + border: 1px solid var(--theme-color-medium, $c-gray); + border-radius: 6px; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + font-size: 14px; + line-height: 1.5; + resize: vertical; +} + +.page-updates__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75em; + margin-top: 0.75em; +} + +.page-updates__copy { + margin: 0; +} + +.page-updates__status { + margin: 0; + font-size: 14px; +} diff --git a/src/components/content/page-updates/page-updates-table.hbs b/src/components/content/page-updates/page-updates-table.hbs new file mode 100644 index 00000000..5c253a28 --- /dev/null +++ b/src/components/content/page-updates/page-updates-table.hbs @@ -0,0 +1,108 @@ +
+

+ Merges sorted by date with affected pages +

+ + {{#if showConfigurator}} +

+ Select the pages that should appear in the home page recent changes list. Copy the generated configuration below into config/recent-pages.js, then restart npm run dev or run npm run build so the home page picks up the changes. Author, date, and commit details come from the selected merge. +

+ {{/if}} + + {{#if merges}} +
    + {{#each merges}} +
  1. +
    +
    + + {{#if changedBy}} + {{> "content/author/author" name=changedBy gravatarUrl=gravatarUrl className="page-updates__author"}} + {{else}} + Updated by unknown author + {{/if}} + {{#if commitUrl}} + + {{commitShortId}} + + {{else}} + Commit unavailable + {{/if}} +
    + {{#if commitMessage}} +

    {{commitMessage}}

    + {{/if}} +
      + {{#each pages}} +
    • + {{#if ../../showConfigurator}} + + {{/if}} + {{#if sectionTitle}} + + Section: {{sectionTitle}} + + {{/if}} + {{title}} + + +{{linesAdded}} −{{linesDeleted}} + +
    • + {{/each}} +
    +
    +
  2. + {{/each}} +
+ {{else}} +

No pages with merge history were found.

+ {{/if}} + + {{#if showConfigurator}} +
+ + +
+ +

+
+
+ {{/if}} +
diff --git a/src/components/content/recent-pages/_recent-pages.scss b/src/components/content/recent-pages/_recent-pages.scss new file mode 100644 index 00000000..00055c4e --- /dev/null +++ b/src/components/content/recent-pages/_recent-pages.scss @@ -0,0 +1,95 @@ +.recent-pages { + @include rem(margin-top, $gutter); +} + +.recent-pages__list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 1em; +} + +.recent-pages__item { + margin: 0; +} + +.recent-pages__link { + padding: 1em; + + display: block; + background: var(--theme-main-color-inverse, $c-white); + border: 1px solid var(--theme-color-medium, $c-gray); + border-radius: 10px; + box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.08); + color: inherit; + text-decoration: none; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease, + transform 0.15s ease; + + &:hover, + &:focus { + border-color: var(--theme-color-dark); + box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12); + transform: translateY(-1px); + text-decoration: none; + } +} + +.recent-pages__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35em 0.5em; + margin-bottom: 0.25em; +} + +.recent-pages__date { + display: inline-block; + margin: 0; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.recent-pages__pill { + display: inline-block; + margin: 0; + padding: 0.1em 0.5em; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + line-height: 1.3; + letter-spacing: 0.01em; + background-color: var(--theme-color-light); + color: var(--theme-color-dark); +} + +.recent-pages__title { + margin: 0; + font-size: 24px; + line-height: 1.25; +} + +.recent-pages__title-link { + color: var(--theme-color-dark); + text-decoration: none; +} + +.recent-pages__lead { + margin-top: 0.75em; + margin-bottom: 1.5em; + font-size: 16px; + line-height: 1.5; +} + +.recent-pages__author { + display: flex; +} + +.recent-pages__overview { + margin: 1em 0 0; +} diff --git a/src/components/content/recent-pages/recent-pages.hbs b/src/components/content/recent-pages/recent-pages.hbs new file mode 100644 index 00000000..c7c1a871 --- /dev/null +++ b/src/components/content/recent-pages/recent-pages.hbs @@ -0,0 +1,35 @@ +
+
+

Recent changes

+
+ + + + {{#if overviewLink}} +

+ {{overviewLink.title}} +

+ {{/if}} +
diff --git a/src/templates/layout.hbs b/src/templates/layout.hbs index f6fa7318..1fcd46eb 100644 --- a/src/templates/layout.hbs +++ b/src/templates/layout.hbs @@ -55,10 +55,19 @@ {{#if subPages}} {{> "content/cardmenu/cardmenu" menuitem=subPages}} {{/if}} + {{#if pageUpdatesList}} + {{> "content/page-updates/page-updates-table" merges=pageUpdatesList showConfigurator=pageUpdatesConfigurator}} + {{/if}} + {{#if allPagesList}} + {{> "content/all-pages/all-pages-table" pages=allPagesList}} + {{/if}} + {{#if recentlyUpdatedPages}} + {{> "content/recent-pages/recent-pages" pages=recentlyUpdatedPages overviewLink=recentPagesOverviewLink}} + {{/if}} {{> "content/prev_next/prev_next" prev=previousPage next=nextPage}} {{#notEq section 'welcome'}} - {{#if changed}} + {{#if showPageMetaInfo}}