@fullstackdatasolutions/articles 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstackdatasolutions/articles",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "funding": {
@@ -358,9 +358,10 @@ describe('getArticleMetadata - frontmatter parsing', () => {
358
358
  })
359
359
  expect(article).not.toBeNull()
360
360
  expect(article!.authorSlug).toBe('andrew-blase')
361
- expect(article!.authorAvatar).toBe(
362
- 'https://example.com/articles/authors/andrew-blase/avatar.jpg'
363
- )
361
+ // Relative, not absolute: authorAvatar is only ever rendered as an
362
+ // <img src>, never used for JSON-LD (which resolves its own absolute
363
+ // URL separately via getAuthorAvatar(author, config) at render time).
364
+ expect(article!.authorAvatar).toBe('/articles/authors/andrew-blase/avatar.jpg')
364
365
  })
365
366
 
366
367
  it('leaves authorSlug and authorAvatar unset when no config is passed', async () => {
package/src/markdown.ts CHANGED
@@ -362,7 +362,9 @@ export async function markdownToHtml(
362
362
  processor = processor.use(rehypeProcessImages, { articleSlug })
363
363
  }
364
364
 
365
- const result = await processor.use(rehypeStringify).process(markdown)
365
+ const result = await processor
366
+ .use(rehypeStringify)
367
+ .process(stripInlineTagsFromHeadings(markdown))
366
368
 
367
369
  return result.toString()
368
370
  } catch (error) {
@@ -377,8 +379,51 @@ export async function markdownToHtml(
377
379
  }
378
380
  }
379
381
 
382
+ /**
383
+ * Strips inline HTML/JSX tags from heading lines only, keeping their inner
384
+ * text, before the markdown reaches a plain (non-MDX) remark parse.
385
+ *
386
+ * Article headings commonly carry a reader-facing rating dot written as JSX,
387
+ * e.g. `### Rage <span style={{ color: '#3b82f6' }}>●</span>`. `renderMdxSource`
388
+ * (real MDX compilation via `@mdx-js/mdx`) parses that correctly and renders
389
+ * a real `<span>` element. But `extractToc` and `markdownToHtml` both run
390
+ * headings through plain `remark-parse` with no MDX support, and CommonMark's
391
+ * raw-inline-HTML grammar does not accept a JSX object-literal attribute
392
+ * expression like `style={{ ... }}` - remark's HTML tokenizer fails to match
393
+ * it as a tag and falls back to treating the whole thing as literal text.
394
+ * That garbled text then (a) becomes the visible TOC label, and (b) feeds
395
+ * `rehype-slug`, producing a slug built from the raw markup instead of the
396
+ * heading's real words - which does not match the id `renderMdxSource`'s
397
+ * correctly-parsed pipeline assigns to the same heading in the live page, so
398
+ * the TOC entry silently links to an id that does not exist in the DOM.
399
+ *
400
+ * Stripping tags (not their inner content) from heading lines before parsing
401
+ * keeps the extracted text and generated slug consistent with what the real
402
+ * MDX render puts in the page, for any heading-level inline markup - not
403
+ * only the rating-dot convention that surfaced the bug.
404
+ */
405
+ function stripInlineTagsFromHeadings(markdown: string): string {
406
+ return markdown.replace(/^(#{1,6}[ \t].*)$/gm, (line) =>
407
+ line.replace(/<\/?[a-zA-Z][^<>\n]*>/g, '')
408
+ )
409
+ }
410
+
411
+ /**
412
+ * Recursively extracts a node's text content, including text nested inside
413
+ * child elements (for example a markdown link's `<a>Dwarf</a>` inside a
414
+ * heading like `### [Dwarf](/link)`). A shallow, direct-children-only check
415
+ * here previously dropped link text from every heading that used a link as
416
+ * part of its heading text - a distinct bug from the JSX-in-heading garbling
417
+ * `stripInlineTagsFromHeadings` fixes, but with a worse symptom: the heading
418
+ * was silently omitted from the TOC entirely (both `id` and `text` came back
419
+ * empty, so `extractHeadingItem` returned `null`) rather than merely garbled.
420
+ */
380
421
  function nodeTextValue(c: ElementContent): string {
381
- return c.type === 'text' ? (c as { value: string }).value : ''
422
+ if (c.type === 'text') return (c as { value: string }).value
423
+ if (c.type === 'element' && 'children' in c) {
424
+ return (c as Element).children.map(nodeTextValue).join('')
425
+ }
426
+ return ''
382
427
  }
383
428
 
384
429
  function extractHeadingItem(node: Element): TocItem | null {
@@ -446,6 +491,6 @@ export async function extractToc(markdown: string): Promise<TocItem[]> {
446
491
  .use(rehypeSlug)
447
492
  .use(collectHeadings)
448
493
  .use(rehypeStringify)
449
- .process(markdown)
494
+ .process(stripInlineTagsFromHeadings(markdown))
450
495
  return headings
451
496
  }
@@ -296,9 +296,7 @@ async function getArticleSummary(slug: string, config?: ArticlesConfig): Promise
296
296
  author,
297
297
  authors,
298
298
  authorSlug: primaryAuthorProfile?.slug,
299
- authorAvatar: primaryAuthorProfile
300
- ? getAuthorAvatar(primaryAuthorProfile, config)
301
- : undefined,
299
+ authorAvatar: primaryAuthorProfile ? getAuthorAvatar(primaryAuthorProfile) : undefined,
302
300
  category: categories[0],
303
301
  categories,
304
302
  readTime,