• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scriptype / writ-cms / 13516829030

25 Feb 2025 08:17AM UTC coverage: 80.486% (-0.8%) from 81.334%
13516829030

push

github

scriptype
Introduce ContentModel2/Collections (WIP)

Start of a major change in content modelling. Develop inside master without touching the working version.

Goal:

- The old contentModel structure becomes a "collection" of "entries".
- Root possibly has many collections.
- Each collection will be of a certain content type. e.g. TextEntry
  Collection
- Collections have categorization and tagging within them
- stuff like: example.com/blog, example.com/projects,
  example.com/podcast (each directory is a collection)

Notes:

- Bypassed bringing date from git during enhancements
- Eliminated everything async from contentModel(2) (for clarity and testability)
- Removed outputPath from categories and posts because it caused issues
  and also seemed it shouldn't be a concern of contentModel to calculate
  outputPath (we'll see).
- Identified posts and categories by their fsObject.path
- Removed subpages and homepage from linking enhancer
- Allowed contentModel hook to be run for each collection
- Computed entry and tag permalinks based on fsObject.path (feels like the containing
  collection should be available to entry scopes)

440 of 652 branches covered (67.48%)

Branch coverage included in aggregate %.

397 of 477 new or added lines in 19 files covered. (83.23%)

2311 of 2766 relevant lines covered (83.55%)

228.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.11
/src/compiler/contentModel2/parseTemplate.js
1
const frontMatter = require('front-matter')
6✔
2
const marked = require('marked')
6✔
3

4
marked.setOptions({
6✔
5
  headerIds: false
6
})
7

8
const READ_MORE_DIVIDER = '{{seeMore}}'
6✔
9

10
const MarkdownHelpers = {
6✔
11
  trimExtraParagraphAroundSeeMore(html) {
12
    const paragraphContainingSeeMore = html.match(/<p>(\s+|)\{\{seeMore\}\}(\s+|)<\/p>/s)
24✔
13
    if (paragraphContainingSeeMore) {
24!
NEW
14
      return html.replace(paragraphContainingSeeMore[0], '{{seeMore}}')
×
15
    }
16
    return html
24✔
17
  },
18

19
  unescapeHandlebarsExpressions(html) {
20
    // partial greater sign
21
    html = html.replace(/{{&gt;/g, '{{>')
24✔
22

23
    // helpers
24
    html = html.replace(/{{(.+)(?:&quot;|&#39;)(.+)(?:&quot;|&#39;)(.*)}}/g, '{{\$1"\$2"\$3}}')
24✔
25

26
    // partials
27
    html = html.replace(/{{>(.+)(?:&quot;|&#39;)(.+)(?:&quot;|&#39;).*}}/g, '{{>\$1"\$2"\$3}}')
24✔
28

29
    return html
24✔
30
  }
31
}
32

33
const getSummary = (content, localAssets, permalink) => {
6✔
34
  let summaryPart = content.split(READ_MORE_DIVIDER)[0]
54✔
35
  if (!localAssets || !localAssets.length) {
54✔
36
    return summaryPart
42✔
37
  }
38
  localAssets.forEach(asset => {
12✔
39
    const assetName = asset.isFolder ? asset.name + '/' : asset.name
24!
40
    const srcRe = new RegExp(`src=("|'|)(\.\/|)${assetName}`, 'g')
24✔
41
    summaryPart = summaryPart.replace(srcRe, `src="${permalink}/${assetName}`)
24✔
42
    const hrefRe = new RegExp(`href=("|'|)(\.\/|)${assetName}`, 'g')
24✔
43
    summaryPart = summaryPart.replace(hrefRe, `href="${permalink}/${assetName}`)
24✔
44
  })
45
  return summaryPart
12✔
46
}
47

48
const getMentions = (content) => {
6✔
49
  const pattern = /{{\s*#?mention (?:'|")(.+)(?:'|")\s*}}/
54✔
50
  // Find all occurrences of mention helper
51
  const matches = content.match(new RegExp(pattern, 'g'))
54✔
52
  if (!matches) {
54!
53
    return []
54✔
54
  }
55
  // Map to permalinks
NEW
56
  return matches.map(match => {
×
NEW
57
    const permalink = match.match(pattern)[1]
×
NEW
58
    return `/${permalink}`.replace(/^\/\//, '/')
×
59
  })
60
}
61

62
const getHTMLContent = (body, extension) => {
6✔
63
  if (/^(\.html|\.htm)$/i.test(extension)) {
54!
NEW
64
    return body
×
65
  }
66
  if (/^(\.md|\.markdown|\.txt)$/i.test(extension)) {
54✔
67
    let compiledHTML = marked.parse(
24✔
68
      body.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, '')
69
    )
70
    compiledHTML = MarkdownHelpers.trimExtraParagraphAroundSeeMore(compiledHTML)
24✔
71
    compiledHTML = MarkdownHelpers.unescapeHandlebarsExpressions(compiledHTML)
24✔
72
    return compiledHTML
24✔
73
  }
74
  return body
30✔
75
}
76

77
const parseTags = (tags = []) => {
6✔
78
  return typeof tags === 'string' ?
54✔
79
    tags.split(',').map(t => t.trim()) :
108✔
80
    tags
81
}
82

83
const parseTemplate = (fsObject, { localAssets, permalink } = {}) => {
6!
84
  const { content, extension } = fsObject
54✔
85
  const { attributes, body } = frontMatter(content)
54✔
86
  const { type, title, cover, media, tags, ...restAttributes } = attributes
54✔
87
  const HTMLContent = getHTMLContent(body, extension)
54✔
88
  const metadata = {
54✔
89
    type,
90
    title,
91
    cover,
92
    media,
93
    content: HTMLContent,
94
    summary: getSummary(HTMLContent, localAssets, permalink),
95
    mentions: getMentions(HTMLContent),
96
    tags: parseTags(tags),
97
    publishDate: attributes.date ? new Date(attributes.date) : null
54✔
98
  }
99
  return {
54✔
100
    ...metadata,
101
    attributes: restAttributes
102
  }
103
}
104

105
module.exports = parseTemplate
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc