storipress/karbon

View on GitHub
packages/karbon/src/runtime/api/normalize-article.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { destr } from 'destr'
import truncate from 'lodash.truncate'
import type { Segment } from '../lib/split-article'
import type { ArticleDesk, ArticlePlan, ArticleTag } from '../types'
import { useArticleFilter } from '#imports'

export interface TypesenseArticleLike {
  id: string
  slug: string
  featured: boolean
  order: number
  updated_at: number
  title: string
  blurb: string
  seo: string
  cover: string
  plan: ArticlePlan
  authors: {
    id: number
    slug: string
    full_name: string
    socials: string
    avatar: string
    bio: string
    location: string
  }[]
  tags: {
    id: number
    name: string
    slug: string
  }[]
  desk: {
    id: number
    name: string
    slug: string
    desk?: undefined
  }
  published_at: string | number
  pathnames: string[]
  html?: undefined
  content?: string
  // make it compatibility with RawArticleLike
  plaintext?: undefined
}
export interface RawUserLike {
  id: string
  slug: string
  full_name: string
  socials: string
  avatar: string
  bio: string
  location: string
}

export interface RawArticleLike {
  id: string
  slug: string
  featured: boolean
  updated_at: string | number
  title: string
  blurb: string
  seo: string
  cover: string
  html: string
  plaintext: string
  plan: ArticlePlan
  authors: RawUserLike[]
  tags: ArticleTag[]
  desk: ArticleDesk
  published_at: string | number
  order?: undefined
  pathnames?: undefined
  // make it compatibility with TypesenseArticleLike
  content?: undefined
}

export interface PaidContent {
  content: string
  key: string
  iv: string
}

export interface RawSEOItem {
  title: string
  description: string
}

export interface RawSEO {
  meta?: RawSEOItem
  og?: RawSEOItem
  ogImage?: string
}

export function normalizeArticle(article: RawArticleLike | TypesenseArticleLike) {
  const {
    title,
    blurb,
    seo,
    cover,
    plan,
    id,
    authors,
    desk,
    tags,
    published_at,
    updated_at,
    html,
    plaintext,
    content,
    ...rest
  } = article

  const articleFilter = useArticleFilter()
  const rootDesk = desk?.desk ? { desk: { ...desk.desk, id: String(desk.desk.id) } } : {}
  const bio = authors?.[0]?.bio ?? ''

  return {
    ...rest,
    id,
    plan,
    bio: articleFilter(bio),
    bioHTML: bio,
    // published_at could be unix timestamp
    published_at: normalizeMaybeUnixTimestamp(published_at),
    updated_at: normalizeMaybeUnixTimestamp(updated_at),
    title: unwrapParagraph(title),
    blurb: unwrapParagraph(blurb),
    seo: destr<RawSEO>(seo),
    html: html ?? '',
    plaintext: truncate(plaintext ?? content, {
      // description length
      length: 120,
      // don't cut on word
      separator: /,? +/,
    }),
    cover: destr(cover),
    authors:
      authors?.map(({ socials, id, ...rest }) => ({
        ...rest,
        id: String(id),
        socials: destr(socials),
        name: rest.full_name,
      })) ?? [],
    desk: {
      ...desk,
      ...rootDesk,
      id: String(desk.id),
    },
    tags: tags?.map(({ id, ...rest }) => ({
      ...rest,
      id: String(id),
    })),
  }
}

export type _NormalizeArticle = ReturnType<typeof normalizeArticle>

export type NormalizeArticle = _NormalizeArticle & {
  paidContent?: PaidContent
  segments: Segment[]
}

function normalizeMaybeUnixTimestamp(date: number | string): string {
  return typeof date === 'string' ? date : new Date(date * 1000).toISOString()
}

export function unwrapParagraph(input: string): string {
  if (!input) {
    return ''
  }

  return input.replace(/^<p>/, '').replace(/<\/p>$/, '')
}