santifer/career-ops · error · Error

Malformed partial: missing <!--ENTRY-->...<!--/ENTRY--> tags

Error message

Malformed partial: missing <!--ENTRY-->...<!--/ENTRY--> tags

What it means

parsePartial in build-cv-html.mjs extracts an HTML entry from a partial file. It first locates an `<!--ENTRY-->...<!--/ENTRY-->` zone with a regex; if the regex finds no match, it throws this error. The library requires every partial to explicitly delimit the entry region so arbitrary surrounding content (comments, build notes) is never stripped or interpreted.

Solutions

  1. Wrap the partial's entry content in literal `<!--ENTRY-->` and `<!--/ENTRY-->` tags.
  2. Verify exact spelling and casing of the tags (uppercase, no internal whitespace: `<!--ENTRY-->`).
  3. Confirm both opening and closing tags are present and not nested inside another comment.
  4. Check the file wasn't transformed (minified/comment-stripped) before reaching parsePartial.

Example fix

<!-- before: bare partial -->
<h2>Experience</h2>
{{EXPERIENCE_ITEMS}}

<!-- after: delimited partial -->
<!--ENTRY-->
<h2>Experience</h2>
{{EXPERIENCE_ITEMS}}
<!--/ENTRY-->
Defensive patterns

Strategy: validation

Validate before calling

function isParseablePartial(source) {
  return typeof source === 'string' && /<!--ENTRY-->[\s\S]*?<!--\/ENTRY-->/.test(source);
}
if (!isParseablePartial(raw)) throw new Error('partial lacks ENTRY markers before calling parsePartial');

Type guard

const isPartialWithEntry = (s) => typeof s === 'string' && s.includes('<!--ENTRY-->') && s.includes('<!--/ENTRY-->');

Try / catch

try {
  const entry = parsePartial(source);
} catch (e) {
  if (e.message.includes('Malformed partial')) {
    console.error(`Partial is missing ENTRY markers: ${partialPath}`);
    return fallbackEntry;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling parsePartial(source) with a source string that lacks literal `<!--ENTRY-->` and `<!--/ENTRY-->` markers — e.g. a partial written as bare HTML, markers misspelled (`<!-- entry -->`, wrong casing, extra spaces inside the comment), a missing closing tag, or an empty file.

Common situations: A new CV section partial was authored without knowing the marker convention; a templating step or editor stripped the HTML comments; a partial was copied from a file that only contains the inner content; tags placed inside a nested comment or generated dynamically so they don't appear literally in the source.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16). Data as JSON: /api/errors/5651f4ea6c1a390c. Report an issue: GitHub.

Appendix: source

Thrown at build-cv-html.mjs:212

//      replaced with '' (or with the <!--BLOCK_NAME_EMPTY--> fallback if one
//      is defined, which the certifications partial uses for alignment).
//
// By keeping documentation outside the ENTRY zone we never need to strip
// arbitrary HTML comments from the entry template, which avoids the
// CodeQL js/incomplete-multi-character-sanitization rule entirely.

// Parse a partial file and return:
//   { entryTemplate: string, blocks: Map<name, {present: string, absent: string}> }
//
// The entry template is extracted from inside <!--ENTRY-->...<!--/ENTRY-->.
// Named conditional-block definitions are extracted from the same zone and
// removed to leave the clean entry template. No HTML comment stripping is
// performed on arbitrary content (no CodeQL sanitization concern).
function parsePartial(source) {
  // Step 1: locate the ENTRY zone.
  const entryZoneMatch = /<!--ENTRY-->([\s\S]*?)<!--\/ENTRY-->/.exec(source);
  if (!entryZoneMatch) {
    throw new Error('Malformed partial: missing <!--ENTRY-->...<!--/ENTRY--> tags');
  }
  const entryZone = entryZoneMatch[1];

  // Step 2: extract named conditional-block definitions from the entry zone.
  const blockRe = /<!--([A-Z][A-Z0-9_]+)-->([\s\S]*?)<!--\/\1-->/g;
  const blocks = new Map();
  // Collect the exact definition strings (open-tag + content + close-tag) so we
  // can remove them verbatim from the entry zone in step 3, without needing any
  // broad HTML-comment regex (which would trigger CodeQL).
  const definitionStrings = [];
  // Collect every definition first. The _EMPTY fallbacks are resolved in a
  // second pass because a fallback may be defined before the block it belongs
  // to, and pairing needs to know which block names exist.
  const definitions = new Map();
  let m;
  while ((m = blockRe.exec(entryZone)) !== null) {
    const name = m[1];
    const content = m[2];

View on GitHub (pinned to e7abd431fc)