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

scriptype / writ-cms / 18128625706

30 Sep 2025 11:38AM UTC coverage: 73.078% (-2.4%) from 75.488%
18128625706

push

github

scriptype
Facets wip5

Handle permalinking from entries to collection facets through its faceted fields. Every facet field becomes an object: { value, facetPermalink }. If the field is a link to another entry (already object), then the facetPermalink property is added on top of existing entry object.

490 of 795 branches covered (61.64%)

Branch coverage included in aggregate %.

8 of 25 new or added lines in 4 files covered. (32.0%)

118 existing lines in 9 files now uncovered.

2276 of 2990 relevant lines covered (76.12%)

313.31 hits per line

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

45.83
/src/compiler/contentModel2/index.js
1
const { resolve } = require('path')
9✔
2
const _ = require('lodash')
9✔
3
const frontMatter = require('front-matter')
9✔
4
const ImmutableStack = require('../../lib/ImmutableStack')
9✔
5
const { isTemplateFile } = require('./helpers')
9✔
6
const models = {
9✔
7
  homepage: require('./models/homepage'),
8
  subpage: require('./models/subpage'),
9
  collection: require('./models/collection'),
10
  asset: require('./models/asset')
11
}
12

13
const linkEntries = (contentModel) => {
9✔
14
  contentModel.collections.forEach(collection => {
9✔
15
    collection.posts.forEach(post => {
9✔
16
      const fields = Object.keys(post)
63✔
17
      const linkFields = fields
63✔
18
        .map(key => {
19
          const match = key.match(/(.+){(.+)}/)
1,182✔
20
          if (!match) {
1,182!
21
            return
1,182✔
22
          }
23
          const [, entrySlug, categorySlug] = post[key].match(/([^(\s]+)(?:\s*\(([^)]+)\))?/)
×
24
          return {
×
25
            key: match[1],
26
            collectionSlug: match[2],
27
            categorySlug,
28
            entrySlug
29
          }
30
        })
31
        .filter(Boolean)
32
      linkFields.forEach(link => {
63✔
33
        const collection = contentModel.collections.find(c => c.slug.match(new RegExp(link.collectionSlug, 'i')))
×
NEW
34
        const container = link.categorySlug ? // TODO: Handle subcategories
×
35
          collection.categories.find(c => c.slug.match(new RegExp(link.categorySlug, 'i'))) || collection :
×
36
          collection
37
        const entry = container.posts.find(p => p.slug.match(new RegExp(link.entrySlug, 'i')))
×
NEW
38
        post[link.key] = Object.assign({}, entry)
×
39
        entry.links = entry.links || {}
×
40
        entry.links.relations = entry.links.relations || []
×
41
        const relation = entry.links.relations.find(r => r.key === link.key)
×
42
        if (relation) {
×
43
          relation.entries.push(post)
×
44
        } else {
45
          entry.links.relations.push({
×
46
            key: link.key,
47
            entries: [post]
48
          })
49
        }
50
      })
51
    })
52
  })
53
}
54

55
const defaultContentModelSettings = {
9✔
56
  permalinkPrefix: '/',
57
  out: resolve('.'),
58
  defaultCategoryName: 'Unclassified',
59
  assetsDirectory: 'assets',
60
  pagesDirectory: 'pages',
61
  homepageDirectory: 'homepage',
62
  debug: false,
63
  site: {
64
    title: '',
65
    description: ''
66
  }
67
}
68
class ContentModel {
69
  constructor(contentModelSettings = defaultContentModelSettings, contentTypes = []) {
9!
70
    this.settings = {
9✔
71
      ...defaultContentModelSettings,
72
      ...contentModelSettings
73
    }
74
    this.contentTypes = contentTypes
9✔
75
  }
76

77
  create(fileSystemTree) {
78
    const indexFileNameOptions = ['root']
9✔
79

80
    const isRootIndexFile = (node) => {
9✔
81
      return isTemplateFile(node) && node.name.match(
18✔
82
        new RegExp(`^(${indexFileNameOptions.join('|')})\\..+$`)
83
      )
84
    }
85

86
    const indexFile = fileSystemTree.find(isRootIndexFile)
9✔
87
    const indexProps = indexFile ? frontMatter(indexFile.content) : {}
9!
88

89
    const context = new ImmutableStack([{
9✔
90
      key: 'root',
91
      outputPath: this.settings.out,
92
      permalink: this.settings.permalinkPrefix
93
    }])
94

95
    this.models = {
9✔
96
      collection: models.collection({
97
        defaultCategoryName: this.settings.defaultCategoryName,
98
        collectionAliases: [
99
          ...this.contentTypes
100
            .filter(ct => ct.model === 'collection')
×
101
            .map(ct => ct.collectionAlias),
×
102
          ...(indexProps.attributes?.collectionAliases || [])
18✔
103
        ]
104
      }, this.contentTypes),
105

106
      subpage: models.subpage({
107
        pagesDirectory: this.settings.pagesDirectory
108
      }),
109

110
      homepage: models.homepage({
111
        homepageDirectory: this.settings.homepageDirectory
112
      }),
113

114
      asset: models.asset({
115
        assetsDirectory: this.settings.assetsDirectory
116
      })
117
    }
118

119
    this.contentModel = {
9✔
120
      homepage: this.models.homepage.create({
121
        name: 'index',
122
        extension: 'md',
123
        content: ''
124
      }, context),
125
      subpages: [],
126
      collections: [],
127
      assets: []
128
    }
129

130
    fileSystemTree.forEach(node => {
9✔
131
      if (this.models.homepage.match(node)) {
18✔
132
        this.contentModel.homepage = this.models.homepage.create(node, context)
9✔
133
        return
9✔
134
      }
135

136
      if (this.models.subpage.match(node)) {
9!
137
        return this.contentModel.subpages.push(
×
138
          this.models.subpage.create(node, context)
139
        )
140
      }
141

142
      if (this.models.subpage.matchPagesDirectory(node)) {
9!
143
        return node.children.forEach(childNode => {
×
144
          if (this.models.subpage.match(childNode)) {
×
145
            this.contentModel.subpages.push(
×
146
              this.models.subpage.create(childNode, context)
147
            )
148
          } else if (this.models.asset.match(childNode)) {
×
149
            this.contentModel.assets.push(
×
150
              this.models.asset.create(childNode, context)
151
            )
152
          }
153
        })
154
      }
155

156
      if (this.models.collection.match(node)) {
9!
157
        return this.contentModel.collections.push(
9✔
158
          this.models.collection.create(node, context)
159
        )
160
      }
161

162
      if (this.models.asset.matchAssetsDirectory(node)) {
×
163
        return this.contentModel.assets.push(
×
164
          ...node.children.map(childNode => {
165
            return this.models.asset.create(childNode, context)
×
166
          })
167
        )
168
      }
169

170
      if (this.models.asset.match(node)) {
×
171
        return this.contentModel.assets.push(
×
172
          this.models.asset.create(node, context)
173
        )
174
      }
175
    })
176

177
    this.afterEffects()
9✔
178
    return this.contentModel
9✔
179
  }
180

181
  afterEffects() {
182
    linkEntries(this.contentModel)
9✔
183

184
    this.contentModel.collections.forEach(collection => {
9✔
185
      this.models.collection.afterEffects(this.contentModel, collection)
9✔
186
    })
187

188
    this.contentModel.subpages.forEach(subpage => {
9✔
UNCOV
189
      this.models.subpage.afterEffects(this.contentModel, subpage)
×
190
    })
191

192
    this.models.homepage.afterEffects(this.contentModel, this.contentModel.homepage)
9✔
193

194
    this.contentModel.assets.forEach(asset => {
9✔
UNCOV
195
      this.models.asset.afterEffects(this.contentModel, asset)
×
196
    })
197
  }
198

199
  render(renderer) {
UNCOV
200
    const renderHomepage = () => {
×
UNCOV
201
      return this.models.homepage.render(renderer, this.contentModel.homepage, {
×
202
        contentModel: this.contentModel,
203
        settings: this.settings,
204
        debug: this.settings.debug
205
      })
206
    }
207

UNCOV
208
    const renderCollections = () => {
×
UNCOV
209
      return Promise.all(
×
210
        this.contentModel.collections.map(collection => {
UNCOV
211
          return this.models.collection.render( renderer, collection, {
×
212
            contentModel: this.contentModel,
213
            settings: this.settings,
214
            debug: this.settings.debug
215
          })
216
        })
217
      )
218
    }
219

UNCOV
220
    const renderSubpages = () => {
×
UNCOV
221
      return Promise.all(
×
222
        this.contentModel.subpages.map(subpage => {
UNCOV
223
          return this.models.subpage.render(renderer, subpage, {
×
224
            contentModel: this.contentModel,
225
            settings: this.settings,
226
            debug: this.settings.debug
227
          })
228
        })
229
      )
230
    }
231

UNCOV
232
    const renderAssets = () => {
×
UNCOV
233
      return Promise.all(
×
234
        this.contentModel.assets.map(asset => {
UNCOV
235
          return this.models.asset.render(renderer, asset, {
×
236
            contentModel: this.contentModel,
237
            settings: this.settings,
238
            debug: this.settings.debug
239
          })
240
        })
241
      )
242
    }
243

UNCOV
244
    return renderHomepage()
×
245
      .then(() =>
UNCOV
246
        Promise.all([
×
247
          renderCollections(),
248
          renderSubpages(),
249
          renderAssets()
250
        ])
251
      )
252
  }
253
}
254

255
module.exports = ContentModel
9✔
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