test(pdf): characterize date layout issues 3155 2841
@@ -0,0 +1,566 @@
|
||||
import type { SemanticNode } from "@reactive-resume/resume/stylesheet";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import type { TextItem } from "pdfjs-dist/types/src/display/api";
|
||||
import type { RasterizedPdfPage } from "../../semantic/test/rasterize-pdf";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pdf, renderToBuffer } from "@react-pdf/renderer";
|
||||
import { encode } from "fast-png";
|
||||
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import { act, createElement } from "react";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { resolveLevelDisplaySizes } from "@reactive-resume/schema/resume/level-display-sizes";
|
||||
import { ResumeDocument } from "../../document";
|
||||
import { resolveResumeRuntime } from "../../semantic/resolve";
|
||||
import { rasterizePdf } from "../../semantic/test/rasterize-pdf";
|
||||
|
||||
const templates = [
|
||||
"azurill",
|
||||
"bronzor",
|
||||
"chikorita",
|
||||
"ditgar",
|
||||
"ditto",
|
||||
"gengar",
|
||||
"glalie",
|
||||
"kakuna",
|
||||
"lapras",
|
||||
"leafish",
|
||||
"meowth",
|
||||
"onyx",
|
||||
"pikachu",
|
||||
"rhyhorn",
|
||||
"scizor",
|
||||
] as const satisfies readonly Template[];
|
||||
|
||||
const dateMarkers = [
|
||||
"EXP_LONG",
|
||||
"EXP_NO_LOCATION",
|
||||
"ROLE_ONE",
|
||||
"ROLE_TWO",
|
||||
"EXP_NO_PERIOD",
|
||||
"EDU_DATE",
|
||||
"PROJECT_DATE",
|
||||
"AWARD_DATE",
|
||||
"CERT_DATE",
|
||||
"VOLUNTEER_DATE",
|
||||
"CUSTOM_EXP_DATE",
|
||||
"CUSTOM_ROLE_DATE",
|
||||
"CUSTOM_EDU_DATE",
|
||||
"CUSTOM_PROJECT_DATE",
|
||||
"CUSTOM_AWARD_DATE",
|
||||
"CUSTOM_CERT_DATE",
|
||||
"CUSTOM_VOLUNTEER_DATE",
|
||||
] as const;
|
||||
|
||||
type DateMarker = (typeof dateMarkers)[number];
|
||||
|
||||
type PdfPageText = {
|
||||
pageNumber: number;
|
||||
items: TextItem[];
|
||||
};
|
||||
|
||||
type RenderedFixture = {
|
||||
bytes: Uint8Array;
|
||||
pages: PdfPageText[];
|
||||
raster: readonly RasterizedPdfPage[];
|
||||
};
|
||||
|
||||
const emptyWebsite = () => ({ url: "", label: "", inlineLink: false });
|
||||
|
||||
const required = <T,>(value: T | undefined): T => {
|
||||
if (value === undefined) throw new Error("Missing fixture value");
|
||||
return value;
|
||||
};
|
||||
|
||||
const richText = (text: string) => `<p>${text}</p>`;
|
||||
|
||||
const dateFixture = (locale: "en-US" | "ar-SA" = "en-US"): ResumeData => {
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.picture.hidden = true;
|
||||
data.basics = {
|
||||
name: locale === "ar-SA" ? "RTL Date Matrix" : "LTR Date Matrix",
|
||||
headline: "Date layout characterization",
|
||||
email: "dates@example.com",
|
||||
phone: "",
|
||||
location: locale === "ar-SA" ? "دبي" : "Berlin",
|
||||
website: { url: "", label: "" },
|
||||
customFields: [],
|
||||
};
|
||||
data.metadata.page.locale = locale;
|
||||
data.metadata.page.marginX = 28;
|
||||
data.metadata.page.marginY = 20;
|
||||
data.metadata.typography.body.fontFamily = "Helvetica";
|
||||
data.metadata.typography.heading.fontFamily = "Helvetica";
|
||||
data.metadata.layout.pages = [
|
||||
{
|
||||
fullWidth: true,
|
||||
main: [
|
||||
"experience",
|
||||
"education",
|
||||
"projects",
|
||||
"awards",
|
||||
"certifications",
|
||||
"publications",
|
||||
"volunteer",
|
||||
"custom-experience",
|
||||
"custom-education",
|
||||
"custom-projects",
|
||||
"custom-awards",
|
||||
"custom-certifications",
|
||||
"custom-publications",
|
||||
"custom-volunteer",
|
||||
],
|
||||
sidebar: [],
|
||||
},
|
||||
];
|
||||
|
||||
data.sections.experience = {
|
||||
...data.sections.experience,
|
||||
title: "Experience matrix",
|
||||
items: [
|
||||
{
|
||||
id: "experience-dated",
|
||||
hidden: false,
|
||||
company: "Experience Parent",
|
||||
position: "Lead Engineer",
|
||||
location: "Oslo LOCATION_ORDER",
|
||||
period: "März 2018 – Dezember 2024 EXP_LONG",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Experience dated details"),
|
||||
roles: [
|
||||
{
|
||||
id: "role-one",
|
||||
position: "Role One",
|
||||
period: "2020 ROLE_ONE",
|
||||
description: richText("First role details"),
|
||||
},
|
||||
{
|
||||
id: "role-two",
|
||||
position: "Role Two",
|
||||
period: "2022 ROLE_TWO",
|
||||
description: richText("Second role details"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "experience-no-location",
|
||||
hidden: false,
|
||||
company: "Experience No Location",
|
||||
position: "Solo Role",
|
||||
location: "",
|
||||
period: "2016 EXP_NO_LOCATION",
|
||||
website: emptyWebsite(),
|
||||
description: "",
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: "experience-no-period",
|
||||
hidden: false,
|
||||
company: "Experience No Period",
|
||||
position: "No Date Role",
|
||||
location: "Oslo EXP_NO_PERIOD",
|
||||
period: "",
|
||||
website: emptyWebsite(),
|
||||
description: "",
|
||||
roles: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.education = {
|
||||
...data.sections.education,
|
||||
title: "Education matrix",
|
||||
items: [
|
||||
{
|
||||
id: "education-dated",
|
||||
hidden: false,
|
||||
school: "Education School",
|
||||
degree: "Degree",
|
||||
area: "Area",
|
||||
grade: "Grade",
|
||||
location: "Education City",
|
||||
period: "2019 EDU_DATE",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Education details"),
|
||||
},
|
||||
{
|
||||
id: "education-empty-period",
|
||||
hidden: false,
|
||||
school: "Education Empty Period",
|
||||
degree: "",
|
||||
area: "",
|
||||
grade: "",
|
||||
location: "",
|
||||
period: "",
|
||||
website: emptyWebsite(),
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.projects = {
|
||||
...data.sections.projects,
|
||||
title: "Projects matrix",
|
||||
items: [
|
||||
{
|
||||
id: "project-dated",
|
||||
hidden: false,
|
||||
name: "Project Entry",
|
||||
period: "2020 PROJECT_DATE",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Project details"),
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.awards = {
|
||||
...data.sections.awards,
|
||||
title: "Awards matrix",
|
||||
items: [
|
||||
{
|
||||
id: "award-dated",
|
||||
hidden: false,
|
||||
title: "Award Entry",
|
||||
awarder: "Award Society",
|
||||
date: "2021 AWARD_DATE",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Award details"),
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.certifications = {
|
||||
...data.sections.certifications,
|
||||
title: "Certifications matrix",
|
||||
items: [
|
||||
{
|
||||
id: "certification-dated",
|
||||
hidden: false,
|
||||
title: "Certification Entry",
|
||||
issuer: "Certification Issuer",
|
||||
date: "2022 CERT_DATE",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Certification details"),
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.publications = {
|
||||
...data.sections.publications,
|
||||
title: "Publications matrix",
|
||||
items: [
|
||||
{
|
||||
id: "publication-empty-date",
|
||||
hidden: false,
|
||||
title: "Publication Empty Date",
|
||||
publisher: "Publication Publisher",
|
||||
date: "",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Publication details"),
|
||||
},
|
||||
],
|
||||
};
|
||||
data.sections.volunteer = {
|
||||
...data.sections.volunteer,
|
||||
title: "Volunteer matrix",
|
||||
items: [
|
||||
{
|
||||
id: "volunteer-dated",
|
||||
hidden: false,
|
||||
organization: "Volunteer Organization",
|
||||
location: "Volunteer City",
|
||||
period: "2023 VOLUNTEER_DATE",
|
||||
website: emptyWebsite(),
|
||||
description: richText("Volunteer details"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
data.customSections = [
|
||||
{
|
||||
...data.sections.experience,
|
||||
id: "custom-experience",
|
||||
type: "experience",
|
||||
title: "Custom Experience matrix",
|
||||
items: [
|
||||
{
|
||||
...required(data.sections.experience.items[0]),
|
||||
id: "custom-experience-item",
|
||||
company: "Custom Experience",
|
||||
location: "Custom Oslo",
|
||||
period: "2024 CUSTOM_EXP_DATE",
|
||||
roles: [
|
||||
{
|
||||
id: "custom-role",
|
||||
position: "Custom Role",
|
||||
period: "2024 CUSTOM_ROLE_DATE",
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...data.sections.education,
|
||||
id: "custom-education",
|
||||
type: "education",
|
||||
title: "Custom Education matrix",
|
||||
items: [
|
||||
{ ...required(data.sections.education.items[0]), id: "custom-education-item", period: "2024 CUSTOM_EDU_DATE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
...data.sections.projects,
|
||||
id: "custom-projects",
|
||||
type: "projects",
|
||||
title: "Custom Projects matrix",
|
||||
items: [
|
||||
{ ...required(data.sections.projects.items[0]), id: "custom-project-item", period: "2024 CUSTOM_PROJECT_DATE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
...data.sections.awards,
|
||||
id: "custom-awards",
|
||||
type: "awards",
|
||||
title: "Custom Awards matrix",
|
||||
items: [{ ...required(data.sections.awards.items[0]), id: "custom-award-item", date: "2024 CUSTOM_AWARD_DATE" }],
|
||||
},
|
||||
{
|
||||
...data.sections.certifications,
|
||||
id: "custom-certifications",
|
||||
type: "certifications",
|
||||
title: "Custom Certifications matrix",
|
||||
items: [
|
||||
{
|
||||
...required(data.sections.certifications.items[0]),
|
||||
id: "custom-certification-item",
|
||||
date: "2024 CUSTOM_CERT_DATE",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...data.sections.publications,
|
||||
id: "custom-publications",
|
||||
type: "publications",
|
||||
title: "Custom Publications matrix",
|
||||
items: [{ ...required(data.sections.publications.items[0]), id: "custom-publication-item", date: "" }],
|
||||
},
|
||||
{
|
||||
...data.sections.volunteer,
|
||||
id: "custom-volunteer",
|
||||
type: "volunteer",
|
||||
title: "Custom Volunteer matrix",
|
||||
items: [
|
||||
{
|
||||
...required(data.sections.volunteer.items[0]),
|
||||
id: "custom-volunteer-item",
|
||||
period: "2024 CUSTOM_VOLUNTEER_DATE",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const extractPages = async (bytes: Uint8Array): Promise<PdfPageText[]> => {
|
||||
const loadingTask = getDocument({ data: bytes, useSystemFonts: true });
|
||||
try {
|
||||
const document = await loadingTask.promise;
|
||||
const pages: PdfPageText[] = [];
|
||||
for (let pageNumber = 1; pageNumber <= document.numPages; pageNumber += 1) {
|
||||
const page = await document.getPage(pageNumber);
|
||||
const content = await page.getTextContent();
|
||||
pages.push({
|
||||
pageNumber,
|
||||
items: content.items.filter(
|
||||
(item): item is TextItem => "str" in item && typeof item.str === "string" && item.str.trim().length > 0,
|
||||
),
|
||||
});
|
||||
}
|
||||
return pages;
|
||||
} finally {
|
||||
await loadingTask.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const renderFixture = async (data: ResumeData, template: Template): Promise<RenderedFixture> => {
|
||||
const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters<typeof renderToBuffer>[0];
|
||||
let bytes = new Uint8Array();
|
||||
await act(async () => {
|
||||
bytes = new Uint8Array(await renderToBuffer(element));
|
||||
});
|
||||
const pages = await extractPages(bytes.slice());
|
||||
const raster = await rasterizePdf(bytes.slice());
|
||||
return { bytes, pages, raster };
|
||||
};
|
||||
|
||||
const allItems = (result: RenderedFixture) => result.pages.flatMap((page) => page.items);
|
||||
|
||||
const textFor = (result: RenderedFixture) =>
|
||||
allItems(result)
|
||||
.map((item) => item.str)
|
||||
.join(" ");
|
||||
|
||||
const markerItems = (result: RenderedFixture, marker: DateMarker) =>
|
||||
allItems(result).filter((item) => new RegExp(`(?:^|\\s)${marker}(?=\\s|$)`).test(item.str));
|
||||
|
||||
const textItems = (result: RenderedFixture, text: string) => allItems(result).filter((item) => item.str.includes(text));
|
||||
|
||||
const coordinatesFor = (result: RenderedFixture, markers: readonly DateMarker[]) =>
|
||||
Object.fromEntries(
|
||||
markers.map((marker) => [
|
||||
marker,
|
||||
markerItems(result, marker).map((item) => ({
|
||||
x: Number(item.transform[4].toFixed(2)),
|
||||
y: Number(item.transform[5].toFixed(2)),
|
||||
width: Number(item.width.toFixed(2)),
|
||||
text: item.str,
|
||||
})),
|
||||
]),
|
||||
);
|
||||
|
||||
const requiredMarkers = (result: RenderedFixture) => {
|
||||
const text = textFor(result);
|
||||
for (const marker of dateMarkers) expect(text).toContain(marker);
|
||||
return coordinatesFor(result, dateMarkers);
|
||||
};
|
||||
|
||||
const writeArtifacts = (name: string, result: RenderedFixture, coordinates: Record<string, unknown>): void => {
|
||||
const output = process.env.DATE_LAYOUT_ARTIFACT_DIR;
|
||||
if (!output) return;
|
||||
mkdirSync(output, { recursive: true });
|
||||
writeFileSync(join(output, `${name}.pdf`), result.bytes);
|
||||
writeFileSync(
|
||||
join(output, `${name}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
pages: result.pages.map((page) => ({ pageNumber: page.pageNumber, itemCount: page.items.length })),
|
||||
coordinates,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
for (const [index, page] of result.raster.entries()) {
|
||||
writeFileSync(join(output, `${name}-page-${index + 1}.png`), encode(page));
|
||||
}
|
||||
};
|
||||
|
||||
type HostNode = {
|
||||
type: string;
|
||||
style?: unknown;
|
||||
value?: string;
|
||||
children?: HostNode[];
|
||||
};
|
||||
|
||||
const nodeText = (node: HostNode): string => node.value ?? (node.children ?? []).map(nodeText).join("");
|
||||
|
||||
const findLink = (node: HostNode, target: string): HostNode | undefined => {
|
||||
if (node.type === "LINK" && nodeText(node) === target) return node;
|
||||
for (const child of node.children ?? []) {
|
||||
const match = findLink(child, target);
|
||||
if (match) return match;
|
||||
}
|
||||
};
|
||||
|
||||
const linkDecoration = async (hideLinkUnderline: boolean) => {
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.picture.hidden = true;
|
||||
data.basics.name = "Link control";
|
||||
data.basics.email = "dates@example.com";
|
||||
data.metadata.page.hideLinkUnderline = hideLinkUnderline;
|
||||
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
|
||||
const instance = pdf(createElement(ResumeDocument, { data, template: "chikorita" }) as never);
|
||||
await expect.poll(() => instance.container.document).not.toBeNull();
|
||||
const link = findLink(instance.container.document as HostNode, data.basics.email);
|
||||
const styles = Array.isArray(link?.style) ? link.style : link?.style ? [link.style] : [];
|
||||
return Object.assign({}, ...styles).textDecoration;
|
||||
};
|
||||
|
||||
const flatten = (node: SemanticNode): SemanticNode[] => [node, ...node.children.flatMap(flatten)];
|
||||
|
||||
describe("date layout characterization (#3155, #2841)", () => {
|
||||
it.each([
|
||||
["chikorita", "ltr"],
|
||||
["ditto", "ltr"],
|
||||
["chikorita", "rtl"],
|
||||
["ditto", "rtl"],
|
||||
] as const)("records Q7 date matrix coordinates and raster for %s %s", async (template, direction) => {
|
||||
const data = dateFixture(direction === "rtl" ? "ar-SA" : "en-US");
|
||||
const result = await renderFixture(data, template);
|
||||
const coordinates = requiredMarkers(result);
|
||||
const text = textFor(result);
|
||||
expect(text).toContain("Publication Empty Date");
|
||||
expect(text).toContain("Custom Publications matrix");
|
||||
const location = textItems(result, "Oslo LOCATION_ORDER");
|
||||
expect(
|
||||
location,
|
||||
location.map((item) => `${item.str}@${item.transform[4]},${item.transform[5]}`).join(" | "),
|
||||
).toHaveLength(1);
|
||||
coordinates.LOCATION_ORDER = location.map((item) => ({
|
||||
x: Number(item.transform[4].toFixed(2)),
|
||||
y: Number(item.transform[5].toFixed(2)),
|
||||
width: Number(item.width.toFixed(2)),
|
||||
text: item.str,
|
||||
}));
|
||||
if (template === "chikorita" && direction === "ltr") {
|
||||
const date = required(markerItems(result, "EXP_LONG")[0]);
|
||||
expect(location[0]?.transform[5]).toBeGreaterThan(date.transform[5]);
|
||||
}
|
||||
for (const marker of dateMarkers) expect(markerItems(result, marker), marker).toHaveLength(1);
|
||||
writeArtifacts(`${template}-${direction}`, result, coordinates);
|
||||
});
|
||||
|
||||
it("records default date evidence for every template without claiming parity geometry", async () => {
|
||||
const evidence: Record<string, unknown> = {};
|
||||
for (const template of templates) {
|
||||
const result = await renderFixture(dateFixture(), template);
|
||||
const coordinates = coordinatesFor(result, dateMarkers);
|
||||
evidence[template] = {
|
||||
pageCount: result.pages.length,
|
||||
textItemCount: allItems(result).length,
|
||||
missingMarkers: dateMarkers.filter((marker) => markerItems(result, marker).length === 0),
|
||||
rasterSha256: result.raster.map((page) => createHash("sha256").update(page.data).digest("hex")),
|
||||
coordinates,
|
||||
};
|
||||
}
|
||||
const output = process.env.DATE_LAYOUT_ARTIFACT_DIR;
|
||||
if (output) {
|
||||
mkdirSync(output, { recursive: true });
|
||||
writeFileSync(join(output, "all-templates.json"), JSON.stringify(evidence, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
it("verifies #2841 link underline toggle independently of date layout", async () => {
|
||||
expect(await linkDecoration(false)).toBe("underline");
|
||||
expect(await linkDecoration(true)).toBe("none");
|
||||
});
|
||||
|
||||
it("verifies #2841 level design variants and explicit size control independently", () => {
|
||||
const data = dateFixture();
|
||||
data.sections.skills.items = [
|
||||
{
|
||||
id: "skill-level-control",
|
||||
hidden: false,
|
||||
icon: "star",
|
||||
iconColor: "",
|
||||
name: "Level control",
|
||||
proficiency: "",
|
||||
level: 3,
|
||||
keywords: [],
|
||||
},
|
||||
];
|
||||
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
|
||||
for (const type of ["hidden", "circle", "square", "rectangle", "rectangle-full", "progress-bar", "icon"] as const) {
|
||||
data.metadata.design.level = { type, icon: "star" };
|
||||
const runtime = resolveResumeRuntime({ data, template: "chikorita", mode: "legacy" });
|
||||
const level = flatten(runtime.sourceTree).find((node) => node.kind === "level");
|
||||
if (type === "hidden") expect(level).toBeUndefined();
|
||||
else expect(level?.children.every((node) => node.attributes.type === type)).toBe(true);
|
||||
}
|
||||
expect(resolveLevelDisplaySizes({ bodyFontSize: 12 })).toEqual({ decorationSize: 10, levelIconExplicitSize: 14 });
|
||||
expect(resolveLevelDisplaySizes({ bodyFontSize: 12, levelFontSize: 7 })).toEqual({
|
||||
decorationSize: 7,
|
||||
levelIconExplicitSize: 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
# Plan 24 Step 1 — date layout characterization
|
||||
|
||||
Revision under test: `66c25efe1` (`origin/main`), branch `codex/issue-3155-date-layout-characterization`.
|
||||
|
||||
The durable fixture is [`date-layout.test.tsx`](../../src/templates/shared/date-layout.test.tsx). It renders the same unchanged resume data through Chikorita and Ditto in LTR and RTL modes, extracts PDF text coordinates, rasterizes each PDF page, and records default evidence for all 15 templates.
|
||||
|
||||
## Matrix coverage
|
||||
|
||||
- Standard date-bearing sections: Awards (`date`), Certifications (`date`), Education (`period`), Experience (`period` and two nested role periods), Projects (`period`), Publications (`date`), and Volunteer (`period`).
|
||||
- Custom equivalents: all seven supported types, including a custom experience role and a custom publication with an empty date.
|
||||
- Edge values: long localized German date, blank location, blank period, mixed dated/undated entries, and RTL locale (`ar-SA`).
|
||||
- #2841 controls: `hideLinkUnderline` resolves independently to `underline`/`none`; all seven level design types are represented in the semantic tree; default and explicit level display sizes resolve independently.
|
||||
|
||||
## Current output evidence
|
||||
|
||||
The Chikorita LTR JSON records current default coordinates such as:
|
||||
|
||||
- Long Experience date: `x=381.24`, `width=186.04`.
|
||||
- Chikorita Experience location: `x=452.35`, `y=706.39`; its date is `y=690.64`, so current PDF coordinates place location before date in the existing split header.
|
||||
- Parent role dates: `x=488.00` and `x=486.54`.
|
||||
- Education period combined with location: `x=416.99`, `width=150.29`.
|
||||
- Project period: `x=465.16`.
|
||||
- Award date: `x=475.41`.
|
||||
- Certification date: `x=484.71`.
|
||||
- Volunteer period: `x=450.91`.
|
||||
|
||||
These are observations of current disabled/default output, not proposed geometry. All-template JSON records page counts, text-item counts, raster SHA-256 values, coordinates, and any missing marker. Meowth omits the location-only marker in the blank-period Experience case; this is recorded as default behavior rather than normalized in the fixture.
|
||||
|
||||
## Issue disposition and gate
|
||||
|
||||
**#3155:** Q4, Q5, Q7, and Q8 are reflected in the fixture contract: optional dedicated date column, logical-start placement, all free-text date sections/custom equivalents, and all templates with default layout preserved. Current output confirms dates are trailing/inline in the existing layout; it does not establish the future dedicated-column geometry.
|
||||
|
||||
**#2841:** Date/location ordering remains a separate layout concern. Existing `hideLinkUnderline` and level design/size controls are present and independently verified; no duplicate schema/UI/runtime controls were added. Historical Chikorita v4 pixel parity remains unverified because no reproducible v4 reference was available.
|
||||
|
||||
**STOP — exact missing visual-geometry gate:** Q6 approves user-controlled per-section width and wrapping but does not specify width units, minimum/maximum bounds, or initial/default width geometry. Narrow-width behavior can be characterized, but implementation must wait for those numeric/geometry inputs; this step intentionally adds no date-column schema, UI, or renderer behavior.
|
||||
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"pageNumber": 1,
|
||||
"itemCount": 49
|
||||
},
|
||||
{
|
||||
"pageNumber": 2,
|
||||
"itemCount": 38
|
||||
}
|
||||
],
|
||||
"coordinates": {
|
||||
"EXP_LONG": [
|
||||
{
|
||||
"x": 381.24,
|
||||
"y": 690.64,
|
||||
"width": 186.04,
|
||||
"text": "März 2018 – Dezember 2024 EXP_LONG"
|
||||
}
|
||||
],
|
||||
"EXP_NO_LOCATION": [
|
||||
{
|
||||
"x": 446.21,
|
||||
"y": 606.64,
|
||||
"width": 121.07,
|
||||
"text": "2016 EXP_NO_LOCATION"
|
||||
}
|
||||
],
|
||||
"ROLE_ONE": [
|
||||
{
|
||||
"x": 488,
|
||||
"y": 674.89,
|
||||
"width": 79.28,
|
||||
"text": "2020 ROLE_ONE"
|
||||
}
|
||||
],
|
||||
"ROLE_TWO": [
|
||||
{
|
||||
"x": 486.54,
|
||||
"y": 643.39,
|
||||
"width": 80.74,
|
||||
"text": "2022 ROLE_TWO"
|
||||
}
|
||||
],
|
||||
"EXP_NO_PERIOD": [
|
||||
{
|
||||
"x": 459.47,
|
||||
"y": 569.89,
|
||||
"width": 107.81,
|
||||
"text": "Oslo EXP_NO_PERIOD"
|
||||
}
|
||||
],
|
||||
"EDU_DATE": [
|
||||
{
|
||||
"x": 416.99,
|
||||
"y": 480.04,
|
||||
"width": 150.29,
|
||||
"text": "Education City • 2019 EDU_DATE"
|
||||
}
|
||||
],
|
||||
"PROJECT_DATE": [
|
||||
{
|
||||
"x": 465.16,
|
||||
"y": 384.94,
|
||||
"width": 102.12,
|
||||
"text": "2020 PROJECT_DATE"
|
||||
}
|
||||
],
|
||||
"AWARD_DATE": [
|
||||
{
|
||||
"x": 475.41,
|
||||
"y": 310.84,
|
||||
"width": 91.87,
|
||||
"text": "2021 AWARD_DATE"
|
||||
}
|
||||
],
|
||||
"CERT_DATE": [
|
||||
{
|
||||
"x": 484.71,
|
||||
"y": 220.99,
|
||||
"width": 82.57,
|
||||
"text": "2022 CERT_DATE"
|
||||
}
|
||||
],
|
||||
"VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 450.91,
|
||||
"y": 41.29,
|
||||
"width": 116.37,
|
||||
"text": "2023 VOLUNTEER_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EXP_DATE": [
|
||||
{
|
||||
"x": 443.13,
|
||||
"y": 723.04,
|
||||
"width": 124.15,
|
||||
"text": "2024 CUSTOM_EXP_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_ROLE_DATE": [
|
||||
{
|
||||
"x": 436.11,
|
||||
"y": 707.29,
|
||||
"width": 131.17,
|
||||
"text": "2024 CUSTOM_ROLE_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EDU_DATE": [
|
||||
{
|
||||
"x": 368.5,
|
||||
"y": 633.19,
|
||||
"width": 198.78,
|
||||
"text": "Education City • 2024 CUSTOM_EDU_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_PROJECT_DATE": [
|
||||
{
|
||||
"x": 416.67,
|
||||
"y": 559.09,
|
||||
"width": 150.61,
|
||||
"text": "2024 CUSTOM_PROJECT_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_AWARD_DATE": [
|
||||
{
|
||||
"x": 426.92,
|
||||
"y": 484.99,
|
||||
"width": 140.36,
|
||||
"text": "2024 CUSTOM_AWARD_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_CERT_DATE": [
|
||||
{
|
||||
"x": 436.22,
|
||||
"y": 395.14,
|
||||
"width": 131.06,
|
||||
"text": "2024 CUSTOM_CERT_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 402.42,
|
||||
"y": 215.44,
|
||||
"width": 164.86,
|
||||
"text": "2024 CUSTOM_VOLUNTEER_DATE"
|
||||
}
|
||||
],
|
||||
"LOCATION_ORDER": [
|
||||
{
|
||||
"x": 452.35,
|
||||
"y": 706.39,
|
||||
"width": 114.93,
|
||||
"text": "Oslo LOCATION_ORDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"pageNumber": 1,
|
||||
"itemCount": 47
|
||||
},
|
||||
{
|
||||
"pageNumber": 2,
|
||||
"itemCount": 41
|
||||
}
|
||||
],
|
||||
"coordinates": {
|
||||
"EXP_LONG": [
|
||||
{
|
||||
"x": 381.24,
|
||||
"y": 684.52,
|
||||
"width": 186.04,
|
||||
"text": "März 2018 – Dezember 2024 EXP_LONG"
|
||||
}
|
||||
],
|
||||
"EXP_NO_LOCATION": [
|
||||
{
|
||||
"x": 446.21,
|
||||
"y": 588.28,
|
||||
"width": 121.07,
|
||||
"text": "EXP_NO_LOCATION 2016"
|
||||
}
|
||||
],
|
||||
"ROLE_ONE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 668.77,
|
||||
"width": 79.28,
|
||||
"text": "ROLE_ONE 2020"
|
||||
}
|
||||
],
|
||||
"ROLE_TWO": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 631.15,
|
||||
"width": 80.74,
|
||||
"text": "ROLE_TWO 2022"
|
||||
}
|
||||
],
|
||||
"EXP_NO_PERIOD": [
|
||||
{
|
||||
"x": 459.47,
|
||||
"y": 551.53,
|
||||
"width": 107.81,
|
||||
"text": "Oslo EXP_NO_PERIOD"
|
||||
}
|
||||
],
|
||||
"EDU_DATE": [
|
||||
{
|
||||
"x": 416.99,
|
||||
"y": 461.68,
|
||||
"width": 150.29,
|
||||
"text": "Education City • 2019 EDU_DATE"
|
||||
}
|
||||
],
|
||||
"PROJECT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 360.46,
|
||||
"width": 102.12,
|
||||
"text": "PROJECT_DATE 2020"
|
||||
}
|
||||
],
|
||||
"AWARD_DATE": [
|
||||
{
|
||||
"x": 475.41,
|
||||
"y": 280.24,
|
||||
"width": 91.87,
|
||||
"text": "AWARD_DATE 2021"
|
||||
}
|
||||
],
|
||||
"CERT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 184.27,
|
||||
"width": 82.57,
|
||||
"text": "CERT_DATE 2022"
|
||||
}
|
||||
],
|
||||
"VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 789.54,
|
||||
"width": 116.37,
|
||||
"text": "VOLUNTEER_DATE 2023"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EXP_DATE": [
|
||||
{
|
||||
"x": 443.13,
|
||||
"y": 677.82,
|
||||
"width": 124.15,
|
||||
"text": "CUSTOM_EXP_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_ROLE_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 662.07,
|
||||
"width": 131.17,
|
||||
"text": "CUSTOM_ROLE_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EDU_DATE": [
|
||||
{
|
||||
"x": 368.5,
|
||||
"y": 587.97,
|
||||
"width": 198.78,
|
||||
"text": "Education City • 2024 CUSTOM_EDU_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_PROJECT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 507.75,
|
||||
"width": 150.61,
|
||||
"text": "CUSTOM_PROJECT_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_AWARD_DATE": [
|
||||
{
|
||||
"x": 426.92,
|
||||
"y": 427.53,
|
||||
"width": 140.36,
|
||||
"text": "CUSTOM_AWARD_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_CERT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 331.56,
|
||||
"width": 131.06,
|
||||
"text": "CUSTOM_CERT_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 139.62,
|
||||
"width": 164.86,
|
||||
"text": "CUSTOM_VOLUNTEER_DATE 2024"
|
||||
}
|
||||
],
|
||||
"LOCATION_ORDER": [
|
||||
{
|
||||
"x": 452.35,
|
||||
"y": 700.27,
|
||||
"width": 114.93,
|
||||
"text": "Oslo LOCATION_ORDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"pageNumber": 1,
|
||||
"itemCount": 47
|
||||
},
|
||||
{
|
||||
"pageNumber": 2,
|
||||
"itemCount": 40
|
||||
}
|
||||
],
|
||||
"coordinates": {
|
||||
"EXP_LONG": [
|
||||
{
|
||||
"x": 381.24,
|
||||
"y": 655.24,
|
||||
"width": 186.04,
|
||||
"text": "März 2018 – Dezember 2024 EXP_LONG"
|
||||
}
|
||||
],
|
||||
"EXP_NO_LOCATION": [
|
||||
{
|
||||
"x": 446.21,
|
||||
"y": 571.24,
|
||||
"width": 121.07,
|
||||
"text": "2016 EXP_NO_LOCATION"
|
||||
}
|
||||
],
|
||||
"ROLE_ONE": [
|
||||
{
|
||||
"x": 488,
|
||||
"y": 639.49,
|
||||
"width": 79.28,
|
||||
"text": "2020 ROLE_ONE"
|
||||
}
|
||||
],
|
||||
"ROLE_TWO": [
|
||||
{
|
||||
"x": 486.54,
|
||||
"y": 607.99,
|
||||
"width": 80.74,
|
||||
"text": "2022 ROLE_TWO"
|
||||
}
|
||||
],
|
||||
"EXP_NO_PERIOD": [
|
||||
{
|
||||
"x": 459.47,
|
||||
"y": 534.49,
|
||||
"width": 107.81,
|
||||
"text": "Oslo EXP_NO_PERIOD"
|
||||
}
|
||||
],
|
||||
"EDU_DATE": [
|
||||
{
|
||||
"x": 416.99,
|
||||
"y": 445.49,
|
||||
"width": 150.29,
|
||||
"text": "Education City • 2019 EDU_DATE"
|
||||
}
|
||||
],
|
||||
"PROJECT_DATE": [
|
||||
{
|
||||
"x": 465.16,
|
||||
"y": 351.24,
|
||||
"width": 102.12,
|
||||
"text": "2020 PROJECT_DATE"
|
||||
}
|
||||
],
|
||||
"AWARD_DATE": [
|
||||
{
|
||||
"x": 475.41,
|
||||
"y": 277.99,
|
||||
"width": 91.87,
|
||||
"text": "2021 AWARD_DATE"
|
||||
}
|
||||
],
|
||||
"CERT_DATE": [
|
||||
{
|
||||
"x": 484.71,
|
||||
"y": 188.99,
|
||||
"width": 82.57,
|
||||
"text": "2022 CERT_DATE"
|
||||
}
|
||||
],
|
||||
"VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 450.91,
|
||||
"y": 812.89,
|
||||
"width": 116.37,
|
||||
"text": "2023 VOLUNTEER_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EXP_DATE": [
|
||||
{
|
||||
"x": 443.13,
|
||||
"y": 708.14,
|
||||
"width": 124.15,
|
||||
"text": "2024 CUSTOM_EXP_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_ROLE_DATE": [
|
||||
{
|
||||
"x": 436.11,
|
||||
"y": 692.39,
|
||||
"width": 131.17,
|
||||
"text": "2024 CUSTOM_ROLE_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EDU_DATE": [
|
||||
{
|
||||
"x": 368.5,
|
||||
"y": 619.14,
|
||||
"width": 198.78,
|
||||
"text": "Education City • 2024 CUSTOM_EDU_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_PROJECT_DATE": [
|
||||
{
|
||||
"x": 416.67,
|
||||
"y": 545.89,
|
||||
"width": 150.61,
|
||||
"text": "2024 CUSTOM_PROJECT_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_AWARD_DATE": [
|
||||
{
|
||||
"x": 426.92,
|
||||
"y": 472.64,
|
||||
"width": 140.36,
|
||||
"text": "2024 CUSTOM_AWARD_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_CERT_DATE": [
|
||||
{
|
||||
"x": 436.22,
|
||||
"y": 383.64,
|
||||
"width": 131.06,
|
||||
"text": "2024 CUSTOM_CERT_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 402.42,
|
||||
"y": 205.64,
|
||||
"width": 164.86,
|
||||
"text": "2024 CUSTOM_VOLUNTEER_DATE"
|
||||
}
|
||||
],
|
||||
"LOCATION_ORDER": [
|
||||
{
|
||||
"x": 452.35,
|
||||
"y": 670.99,
|
||||
"width": 114.93,
|
||||
"text": "Oslo LOCATION_ORDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"pageNumber": 1,
|
||||
"itemCount": 46
|
||||
},
|
||||
{
|
||||
"pageNumber": 2,
|
||||
"itemCount": 42
|
||||
}
|
||||
],
|
||||
"coordinates": {
|
||||
"EXP_LONG": [
|
||||
{
|
||||
"x": 381.24,
|
||||
"y": 649.12,
|
||||
"width": 186.04,
|
||||
"text": "März 2018 – Dezember 2024 EXP_LONG"
|
||||
}
|
||||
],
|
||||
"EXP_NO_LOCATION": [
|
||||
{
|
||||
"x": 446.21,
|
||||
"y": 552.88,
|
||||
"width": 121.07,
|
||||
"text": "EXP_NO_LOCATION 2016"
|
||||
}
|
||||
],
|
||||
"ROLE_ONE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 633.37,
|
||||
"width": 79.28,
|
||||
"text": "ROLE_ONE 2020"
|
||||
}
|
||||
],
|
||||
"ROLE_TWO": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 595.75,
|
||||
"width": 80.74,
|
||||
"text": "ROLE_TWO 2022"
|
||||
}
|
||||
],
|
||||
"EXP_NO_PERIOD": [
|
||||
{
|
||||
"x": 459.47,
|
||||
"y": 516.13,
|
||||
"width": 107.81,
|
||||
"text": "Oslo EXP_NO_PERIOD"
|
||||
}
|
||||
],
|
||||
"EDU_DATE": [
|
||||
{
|
||||
"x": 416.99,
|
||||
"y": 427.13,
|
||||
"width": 150.29,
|
||||
"text": "Education City • 2019 EDU_DATE"
|
||||
}
|
||||
],
|
||||
"PROJECT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 326.76,
|
||||
"width": 102.12,
|
||||
"text": "PROJECT_DATE 2020"
|
||||
}
|
||||
],
|
||||
"AWARD_DATE": [
|
||||
{
|
||||
"x": 475.41,
|
||||
"y": 247.39,
|
||||
"width": 91.87,
|
||||
"text": "AWARD_DATE 2021"
|
||||
}
|
||||
],
|
||||
"CERT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 152.27,
|
||||
"width": 82.57,
|
||||
"text": "CERT_DATE 2022"
|
||||
}
|
||||
],
|
||||
"VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 749.27,
|
||||
"width": 116.37,
|
||||
"text": "VOLUNTEER_DATE 2023"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EXP_DATE": [
|
||||
{
|
||||
"x": 443.13,
|
||||
"y": 638.4,
|
||||
"width": 124.15,
|
||||
"text": "CUSTOM_EXP_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_ROLE_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 622.65,
|
||||
"width": 131.17,
|
||||
"text": "CUSTOM_ROLE_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_EDU_DATE": [
|
||||
{
|
||||
"x": 368.5,
|
||||
"y": 549.4,
|
||||
"width": 198.78,
|
||||
"text": "Education City • 2024 CUSTOM_EDU_DATE"
|
||||
}
|
||||
],
|
||||
"CUSTOM_PROJECT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 470.03,
|
||||
"width": 150.61,
|
||||
"text": "CUSTOM_PROJECT_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_AWARD_DATE": [
|
||||
{
|
||||
"x": 426.92,
|
||||
"y": 390.66,
|
||||
"width": 140.36,
|
||||
"text": "CUSTOM_AWARD_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_CERT_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 295.54,
|
||||
"width": 131.06,
|
||||
"text": "CUSTOM_CERT_DATE 2024"
|
||||
}
|
||||
],
|
||||
"CUSTOM_VOLUNTEER_DATE": [
|
||||
{
|
||||
"x": 28,
|
||||
"y": 105.3,
|
||||
"width": 164.86,
|
||||
"text": "CUSTOM_VOLUNTEER_DATE 2024"
|
||||
}
|
||||
],
|
||||
"LOCATION_ORDER": [
|
||||
{
|
||||
"x": 452.35,
|
||||
"y": 664.87,
|
||||
"width": 114.93,
|
||||
"text": "Oslo LOCATION_ORDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||