Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions config/roadmaps/PHP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"library": "PHP",
"branches": [
{
"version": "8.2",
"status": "released",
"plannedReleaseDate": null,
"releaseDate": "2022-12-08",
"activeSupportUntil": "2024-12-31",
"securitySupportUntil": "2026-12-31",
"php": null,
"notes": null
},
{
"version": "8.3",
"status": "released",
"plannedReleaseDate": null,
"releaseDate": "2023-11-23",
"activeSupportUntil": "2025-12-31",
"securitySupportUntil": "2027-12-31",
"php": null,
"notes": null
},
{
"version": "8.4",
"status": "released",
"plannedReleaseDate": null,
"releaseDate": "2024-11-21",
"activeSupportUntil": "2026-12-31",
"securitySupportUntil": "2028-12-31",
"php": null,
"notes": null
},
{
"version": "8.5",
"status": "released",
"plannedReleaseDate": null,
"releaseDate": "2025-11-20",
"activeSupportUntil": "2027-12-31",
"securitySupportUntil": "2029-12-31",
"php": null,
"notes": null
}
]
}
4 changes: 2 additions & 2 deletions config/roadmaps/window.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"startDate": "2024-01-01",
"endDate": "2029-12-31"
"startDate": "2025-01-01",
"endDate": "2029-03-31"
}
39 changes: 38 additions & 1 deletion docs/shared/support-timeline-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The forecast and actual release may differ; the generator does not require one t

A `null` end date does not mean indefinite support. Only announced, placeable periods are drawn.
The implementation uses UTC calendar days and treats support end dates as inclusive; interval geometry ends at the start of the following day.
No countdown, elapsed duration, current-date marker or automatic release-state promotion is generated.
No countdown, elapsed duration, current-date marker or automatic release-state promotion is generated in the standalone SVG.

## Node.js API

Expand All @@ -122,6 +122,8 @@ const svg = generateSupportTimeline(roadmap, window);

The validators and `RoadmapDataError` are exported from `scripts/roadmap-data.mjs`.
`options.width` defaults to `960` and must be a safe integer of at least `360` pixels.
`options.phpReference` optionally accepts a PHP roadmap object, validated by `validatePhpReference(input)` from the same module.
Omitting it leaves the standalone library calendar unchanged. The website build supplies the shared PHP configuration automatically.
The SVG may be wider than requested to preserve at least 54 pixels per displayed year; its `viewBox` preserves all content when scaled.
Long headings wrap. Branch names appear inside active-support bars; detailed dates and notes remain in the accessible description and support table.
The opaque ivory background, site-palette text, patterned security bars and distinct marker shapes remain readable independently of the surrounding theme.
Expand Down Expand Up @@ -225,3 +227,38 @@ Images are linked so readers can open and zoom them. HTML calendars and tables s
the table retains semantic column headers and the standalone SVG retains its own title, description and legend.
Portable Markdown includes the complete GFM table and relative image reference, without build metadata, MDX or raw HTML.
The [shared assets publication contract](./shared-assets-publication-contract.md) validates and publishes the generated images alongside other shared resources.

## Maintaining the PHP reference bars

Edit the exact file `config/roadmaps/PHP.json` to maintain the secondary reference bars shared by all configured library timelines.
It uses the same roadmap model: `library` is `PHP`, and each branch's `version` is a major.minor value such as `8.4`.
Keep every field, using `null` for inapplicable Composer constraints and notes. The same date, chronology, uniqueness and unknown-field validation applies.
Use explicit release and support dates from the official [PHP Supported Versions page](https://www.php.net/supported-versions.php).
The initial configuration records PHP 8.2, 8.3, 8.4 and 8.5 from that page, checked on October 29, 2026.
For historical branches, consult PHP's [end-of-life branches](https://www.php.net/eol.php).
Do not calculate dates from support durations or copy these dates into LongitudeOne library configurations.

The build places intersecting PHP periods directly after the library bars on the same display window and date axis.
The compact bars are 16 pixels high, compared with 24 pixels for library bars, with muted solid active support and hatched security-only support.
Only labels such as `PHP 8.4` are visible; full dates remain in the non-visible SVG description, with no PHP tooltip commentary.
Labels are centered in the visible active-support segment when it has enough room; otherwise they start in the hatched security segment.
Label placement leaves space for continuation arrows. An active-only sliver retains its readable label within the calendar bounds.
Labels keep their font size even when a period is clipped to a few days. The existing horizontal scroll and zoom preserve readability on narrow screens.

PHP remains reference data: it creates no landing page, documentation-version list, navigation entry or support table.
Both HTML and portable Markdown use the same generated SVG asset. Missing or unpopulated PHP data adds no reference bars;
invalid non-empty data fails before any images are replaced. PHP data never establishes a missing or empty library roadmap.
When adding a PHP version, verify its announced dates, then run the roadmap tests, full tests, build and Markdown lint.
Inspect representative released and planned library timelines in both site themes and at mobile widths.

## Browser date marker

The website adds a one-pixel muted red line across the library and PHP bar area at the visitor's current local calendar day.
Browser JavaScript calculates its position from the same calendar geometry used to generate the SVG, including the inclusive final display day.
The line refreshes at local midnight and when a suspended page becomes visible again; no daily build is required.
It follows the image when resized or scrolled, leaves continuation arrows unchanged, and disappears when today is outside the display window.
An accessible non-visible label identifies the date. No visible date label or additional legend is added.

This is a website enhancement, not a support commitment. Without JavaScript, the complete static calendar and support table remain available.
Standalone SVGs and portable Markdown retain the same script-free calendar assets and announced dates; they do not include a current-date marker.
Use the Markdown page's HTML counterpart link to view the live marker.
2 changes: 1 addition & 1 deletion docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const config = {
staticDirectories: ['static', '.generated-markdown'],
trailingSlash: false,
headTags: isDevelopment ? [{ tagName: 'meta', attributes: { name: 'robots', content: 'noindex, follow' } }] : [],
clientModules: ['./src/resource-route-aliases.js'],
clientModules: ['./src/resource-route-aliases.js', './src/roadmap-today-client.js'],
onBrokenLinks: 'throw',
markdown: {
parseFrontMatter: parseRepresentationFrontMatter,
Expand Down
12 changes: 12 additions & 0 deletions scripts/roadmap-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ export function validateRoadmapData(input) {
return { library, branches };
}

/** PHP uses the roadmap model as shared reference data, never as a documented library. */
export function validatePhpReference(input) {
const roadmap = validateRoadmapData(input);
if (roadmap.library !== 'PHP') fail({ library: roadmap.library, branch: '<configuration>' }, 'library', 'must be PHP for shared PHP reference data.');
for (const branch of roadmap.branches) {
if (!/^[0-9]+\.[0-9]+$/u.test(branch.version)) {
fail({ library: 'PHP', branch: branch.version }, 'version', 'must be a PHP major.minor version, for example 8.4.');
}
}
return roadmap;
}

export function validateTimelineWindow(input, library = '<global window>') {
const context = { library, branch: '<display window>' };
record(input, ['startDate', 'endDate'], context, 'window');
Expand Down
21 changes: 14 additions & 7 deletions scripts/roadmap-publication.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { readdir, readFile, mkdir, writeFile, rm } from 'node:fs/promises';
import path from 'node:path';
import { load } from 'js-yaml';
import { validateRoadmapData, validateTimelineWindow, RoadmapDataError } from './roadmap-data.mjs';
import { validateRoadmapData, validatePhpReference, validateTimelineWindow, RoadmapDataError } from './roadmap-data.mjs';
import { discoverLibraries } from './library-versions.mjs';
import { baseUrl } from './site-settings.mjs';
import { generateSupportTimeline } from './roadmap-timeline.mjs';
import { buildSupportTimeline } from './roadmap-timeline.mjs';

const text = (value) => ({ type: 'text', value });
const paragraph = (value) => ({ type: 'paragraph', children: [text(value)] });
Expand All @@ -24,13 +24,15 @@ function documentationVersionNodes(library, versions, html) {
}

/** Portable MDAST shared by Docusaurus and Markdown export; no HTML or MDX. */
export function roadmapSupportNodes(input, { library = input?.library, versions = [], html = false } = {}) {
export function roadmapSupportNodes(input, { library = input?.library, versions = [], html = false, timeline = null } = {}) {
if (input === null) return [paragraph('The roadmap for this library has not been established yet.'), ...documentationVersionNodes(library, versions, html)];
const roadmap = validateRoadmapData(input);
if (!roadmap.branches.length) return roadmapSupportNodes(null, { library: roadmap.library, versions, html });
const image = `../assets/roadmaps/${roadmap.library}.svg`;
const children = [heading('Release timeline'), {
type: 'paragraph', data: { hProperties: { className: ['roadmap-timeline'] } }, children: [{ type: 'link', url: image, children: [{
type: 'paragraph', data: { hProperties: { className: ['roadmap-timeline'],
...(html && timeline ? { 'data-timeline-layout': JSON.stringify(timeline) } : {}),
} }, children: [{ type: 'link', url: image, children: [{
type: 'image', url: image,
alt: `${roadmap.library} release and support timeline. Planned releases use diamonds; actual releases use circles. Full dates and PHP compatibility follow in the support table.`,
}] }],
Expand Down Expand Up @@ -69,10 +71,10 @@ export function appendRoadmapSupport(tree, sourcePath, roadmaps, { html = false
if (!/^[^/]+\/index\.md$/.test(sourcePath)) return;
const library = sourcePath.split('/')[0];
if (!Object.hasOwn(roadmaps, library)) return;
const { roadmap, versions } = roadmaps[library];
const { roadmap, versions, timeline } = roadmaps[library];
// Refresh only build-owned nodes; authored content (including metadata) stays in order.
const authored = tree.children.filter((node) => !node.data?.generatedRoadmapSupport);
const generated = roadmapSupportNodes(roadmap, { library, versions, html });
const generated = roadmapSupportNodes(roadmap, { library, versions, html, timeline });
for (const node of generated) node.data = { ...node.data, generatedRoadmapSupport: true };
tree.children = [...authored, ...generated];
}
Expand Down Expand Up @@ -106,8 +108,11 @@ export async function generateRoadmapAssets(root) {
try { configurations = (await readdir(directory)).filter((name) => name.endsWith('.json') && name !== 'window.json'); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
for (const filename of configurations) {
if (filename === 'PHP.json') continue;
if (!Object.hasOwn(libraries, filename.slice(0, -5))) throw new Error(`Roadmap configuration ${filename} has no documented library.`);
}
const phpInput = await optionalRoadmap(path.join(directory, 'PHP.json'), 'PHP');
const phpReference = phpInput === null ? null : validatePhpReference(phpInput);
const roadmaps = {};
const images = [];
let window;
Expand All @@ -130,7 +135,9 @@ export async function generateRoadmapAssets(root) {
try { window = validateTimelineWindow(JSON.parse(await readFile(filename, 'utf8'))); }
catch (error) { throw new Error(`Cannot read roadmap display window ${filename}: ${error.message}`, { cause: error }); }
}
images.push([`${library}.svg`, generateSupportTimeline(roadmap, window)]);
const { svg, layout } = buildSupportTimeline(roadmap, window, { phpReference });
roadmaps[library].timeline = layout;
images.push([`${library}.svg`, svg]);
}
}
const destination = path.join(root, 'static/assets/roadmaps');
Expand Down
Loading
Loading