@feelpp/antora-extensions 1.0.0-rc.1 → 1.0.0-rc.3
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/README.md +59 -0
- package/data/css/listing.css +79 -0
- package/data/helpers/image-spec.js +17 -0
- package/data/helpers/listing-get-sections.js +43 -0
- package/data/helpers/resolve-resource-url.js +13 -0
- package/data/partials/listing-card.hbs +13 -0
- package/data/partials/listing.hbs +16 -0
- package/package.json +9 -1
- package/src/index.js +7 -3
- package/src/lazy-readable.js +19 -0
- package/src/listing.js +108 -0
- package/src/lunr.js +145 -0
- package/src/template.js +3 -0
- package/src/toolbox-navigation.js +5 -3
- package/tests/lunr-test.js +36 -0
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
1
|
# Antora Extensions by Feel++
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
A set of Antora extensions.
|
|
6
|
+
|
|
7
|
+
## Extensions
|
|
8
|
+
|
|
9
|
+
### Lunr Search Index
|
|
10
|
+
|
|
11
|
+
Automatically generates a Lunr.js compatible search index during the Antora build process.
|
|
12
|
+
|
|
13
|
+
#### Usage
|
|
14
|
+
|
|
15
|
+
Add the extension to your `site.yml`:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
antora:
|
|
19
|
+
extensions:
|
|
20
|
+
- '@feelpp/antora-extensions'
|
|
21
|
+
|
|
22
|
+
config:
|
|
23
|
+
lunr:
|
|
24
|
+
indexFile: 'search-index.json' # Output filename (optional)
|
|
25
|
+
maxContentLength: 1000 # Max content per document (optional)
|
|
26
|
+
minContentLength: 50 # Min content to include document (optional)
|
|
27
|
+
debug: false # Enable debug logging (optional)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The search index will be automatically generated at `build/site/search-index.json` after the site is built.
|
|
31
|
+
|
|
32
|
+
#### Features
|
|
33
|
+
|
|
34
|
+
- **Zero configuration**: Works out of the box with sensible defaults
|
|
35
|
+
- **Smart content extraction**: Focuses on main content, excludes navigation and footers
|
|
36
|
+
- **Configurable**: Customize content length limits and output location
|
|
37
|
+
- **Performance optimized**: Efficient processing during site generation
|
|
38
|
+
- **Error handling**: Graceful handling of malformed HTML or missing content
|
|
39
|
+
|
|
40
|
+
## Listing
|
|
41
|
+
|
|
42
|
+
### UI assumptions
|
|
43
|
+
|
|
44
|
+
This extension relies on a contract with the UI in order to minimize the configuration the user must perform to get the extension working.
|
|
45
|
+
|
|
46
|
+
#### Environment variable
|
|
47
|
+
|
|
48
|
+
When this extension is enabled, it sets the `SITE_LISTING_EXTENSION_ENABLED` environment variable to the value `true`.
|
|
49
|
+
This variable is available to the UI templates as `env.SITE_LISTING_EXTENSION_ENABLED`.
|
|
50
|
+
The existence of this variable informs the UI template that the listing extension is enabled.
|
|
51
|
+
When this variable is set, the UI is expected to add certain elements to support the extension.
|
|
52
|
+
|
|
53
|
+
#### Listing styles
|
|
54
|
+
|
|
55
|
+
This package provides additional CSS to style the listing results (`data/css/listing.css`).
|
|
56
|
+
The creator of the UI can either bundle those styles or reference them (for instance in `head-styles.hbs`):
|
|
57
|
+
|
|
58
|
+
```hbs
|
|
59
|
+
{{#if env.SITE_LISTING_EXTENSION_ENABLED}}
|
|
60
|
+
<link rel="stylesheet" href="{{{uiRootPath}}}/css/listing.css">
|
|
61
|
+
{{/if}}
|
|
62
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
.listing .listing-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
justify-content: flex-start;
|
|
5
|
+
margin-top: 5em;
|
|
6
|
+
margin-bottom: 5em;
|
|
7
|
+
gap: clamp(1.5em, 2vw, 5em);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.listing .listing-container .listing-card-container {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex: 1 calc(33.33% - 5em);
|
|
13
|
+
max-width: 500px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Media query for small screens */
|
|
17
|
+
@media (max-width: 767px) {
|
|
18
|
+
.listing .listing-container .listing-card-container {
|
|
19
|
+
flex: 1 250px;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Media query for large screens */
|
|
24
|
+
@media (min-width: 768px) {
|
|
25
|
+
.listing .listing-container .listing-card-container {
|
|
26
|
+
min-width: 250px;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.listing .listing-card:hover,
|
|
31
|
+
.listing .listing-card:focus {
|
|
32
|
+
box-shadow: 0 6px 12px 1px #104d92ad;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.listing .listing-card {
|
|
37
|
+
box-shadow: 0 3px 6px 1px #e0e0e0;
|
|
38
|
+
padding: 1.5em 1em 1em;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
color: initial;
|
|
42
|
+
border-radius: 0.5em;
|
|
43
|
+
flex: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.listing .listing-card-illustration {
|
|
47
|
+
height: 150px;
|
|
48
|
+
object-fit: contain;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
padding-left: 1em;
|
|
53
|
+
padding-right: 1em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.listing .listing-card-illustration > img {
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
object-fit: cover;
|
|
60
|
+
border-radius: 0.25em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.listing .listing-card .listing-card-body {
|
|
64
|
+
border-top: 1px solid #e1e1e1;
|
|
65
|
+
margin-top: 1rem;
|
|
66
|
+
padding-bottom: 0.75rem;
|
|
67
|
+
margin-left: 0.75rem;
|
|
68
|
+
margin-right: 0.75rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.listing .listing-card .listing-card-body p {
|
|
72
|
+
font-size: 0.9em;
|
|
73
|
+
color: #56595c;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.listing .listing-card .listing-card-body h4 {
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
margin-bottom: 0.5rem;
|
|
79
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds image$ family if missing
|
|
5
|
+
* @param {string} spec
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
module.exports = (spec) => {
|
|
9
|
+
if (!spec || spec.includes('image$') || spec.startsWith('http://') || spec.startsWith('https://')) {
|
|
10
|
+
return spec
|
|
11
|
+
}
|
|
12
|
+
const lastColon = spec.lastIndexOf(':')
|
|
13
|
+
if (lastColon > 0 && lastColon < spec.length - 1) {
|
|
14
|
+
return spec.slice(0, lastColon + 1) + 'image$' + spec.slice(lastColon + 1)
|
|
15
|
+
}
|
|
16
|
+
return 'image$' + spec
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const listingConfig = ${listingConfig}
|
|
4
|
+
|
|
5
|
+
module.exports = (configName, page, {data: {root}}) => {
|
|
6
|
+
const {contentCatalog} = root
|
|
7
|
+
if (configName in listingConfig) {
|
|
8
|
+
const config = listingConfig[configName]
|
|
9
|
+
if (config) {
|
|
10
|
+
return config.map((section) => {
|
|
11
|
+
const selector = {
|
|
12
|
+
tag: section.tag,
|
|
13
|
+
withinParentModule: section.withinParentModule
|
|
14
|
+
}
|
|
15
|
+
const pages = getPages(contentCatalog, page, selector)
|
|
16
|
+
return {
|
|
17
|
+
title: section.sectionTitle,
|
|
18
|
+
pages,
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPages = function (contentCatalog, page, {tag, withinParentModule}) {
|
|
27
|
+
const pages = contentCatalog.getPages(({asciidoc, out, src}) => {
|
|
28
|
+
if (!out || !asciidoc) return
|
|
29
|
+
if (src.component !== page.componentVersion.name ||
|
|
30
|
+
(withinParentModule && src.module !== page.module) ||
|
|
31
|
+
src.version !== page.componentVersion.version) return
|
|
32
|
+
const pageTags = asciidoc.attributes['page-tags']
|
|
33
|
+
return pageTags && pageTags.split(',').map((v) => v.trim()).includes(tag)
|
|
34
|
+
}).sort((a, b) => (a.title || '').localeCompare((b.title || '')))
|
|
35
|
+
if (pages && pages.length > 0) {
|
|
36
|
+
while (pages.length % 3 !== 0) {
|
|
37
|
+
pages.push({
|
|
38
|
+
empty: true,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return pages
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = (spec, { data, hash: context }) => {
|
|
4
|
+
if (!spec) return
|
|
5
|
+
const { contentCatalog, page } = data.root
|
|
6
|
+
if (page.component) {
|
|
7
|
+
context = Object.assign({ component: page.component.name, version: page.version, module: page.module }, context)
|
|
8
|
+
}
|
|
9
|
+
const file = contentCatalog.resolveResource(spec, context, 'page', ['page', 'attachment', 'image'])
|
|
10
|
+
if (file) {
|
|
11
|
+
return file.pub.url
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{{#if empty}}
|
|
2
|
+
<div class="empty"></div>
|
|
3
|
+
{{else}}
|
|
4
|
+
<a class="listing-card" href="{{{relativize pub.url}}}">
|
|
5
|
+
<div class="listing-card-illustration">
|
|
6
|
+
<img alt="" src="{{relativize (resolve-resource-url (image-spec asciidoc.attributes.page-illustration))}}"/>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="listing-card-body">
|
|
9
|
+
<h4>{{title}}</h4>
|
|
10
|
+
<p class="listing-card-description">{{{asciidoc.attributes.description}}}</p>
|
|
11
|
+
</div>
|
|
12
|
+
</a>
|
|
13
|
+
{{/if}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="listing">
|
|
2
|
+
{{#with (listing-get-sections configName @root.page)}}
|
|
3
|
+
{{#if this.length}}
|
|
4
|
+
{{#each this}}
|
|
5
|
+
{{#if this.pages.length}}
|
|
6
|
+
<h2>{{{this.title}}}</h2>
|
|
7
|
+
<div class="listing-container">
|
|
8
|
+
{{#each this.pages}}
|
|
9
|
+
<div class="listing-card-container">{{> listing-card this}}</div>
|
|
10
|
+
{{/each}}
|
|
11
|
+
</div>
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{/each}}
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{/with}}
|
|
16
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelpp/antora-extensions",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.3",
|
|
5
5
|
"description": "Antora Extensions by Feel++.",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"repository": {
|
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
"keywords": [],
|
|
12
12
|
"author": "Guillaume Grossetie <ggrossetie@yuzutech.fr>",
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "mocha tests/*-test.js"
|
|
16
|
+
},
|
|
14
17
|
"bugs": {
|
|
15
18
|
"url": "https://github.com/feelpp/feelpp-antora-extensions/issues"
|
|
16
19
|
},
|
|
17
20
|
"homepage": "https://github.com/feelpp/feelpp-antora-extensions#readme",
|
|
18
21
|
"devDependencies": {
|
|
22
|
+
"chai": "^4.3.7",
|
|
23
|
+
"mocha": "^10.2.0",
|
|
19
24
|
"libnpmpublish": "^4.0.2",
|
|
20
25
|
"pacote": "^12.0.2"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"jsdom": "^20.0.3"
|
|
21
29
|
}
|
|
22
30
|
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {register: registerTooboxNavigation} = require('./toolbox-navigation.js')
|
|
2
|
+
const {register: registerListing} = require('./listing.js')
|
|
3
|
+
const {register: registerLunr} = require('./lunr.js')
|
|
2
4
|
|
|
3
|
-
module.exports.register = (
|
|
4
|
-
registerTooboxNavigation(
|
|
5
|
+
module.exports.register = (thisContext, args) => {
|
|
6
|
+
registerTooboxNavigation.call(thisContext, args)
|
|
7
|
+
registerListing.call(thisContext, args)
|
|
8
|
+
registerLunr.call(thisContext, args)
|
|
5
9
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PassThrough } = require('stream')
|
|
4
|
+
|
|
5
|
+
// adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
|
|
6
|
+
class LazyReadable extends PassThrough {
|
|
7
|
+
constructor (fn, options) {
|
|
8
|
+
super(options)
|
|
9
|
+
this._read = function () {
|
|
10
|
+
delete this._read // restores original method
|
|
11
|
+
fn.call(this, options).on('error', this.emit.bind(this, 'error')).pipe(this)
|
|
12
|
+
return this._read.apply(this, arguments)
|
|
13
|
+
}
|
|
14
|
+
this.emit('readable')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = LazyReadable
|
|
19
|
+
|
package/src/listing.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// The name of the package in order to give the Antora logger a useful name
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const ospath = require('path')
|
|
6
|
+
const { promises: fsp } = fs
|
|
7
|
+
const LazyReadable = require('./lazy-readable')
|
|
8
|
+
const template = require('./template')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @module listing-extension
|
|
12
|
+
*/
|
|
13
|
+
function register ({ config: { listing, ...unknownOptions } }) {
|
|
14
|
+
const packageName = "@feelpp/antora-listing-extension"
|
|
15
|
+
const logger = this.getLogger(packageName)
|
|
16
|
+
|
|
17
|
+
if (Object.keys(unknownOptions).length) {
|
|
18
|
+
const keys = Object.keys(unknownOptions)
|
|
19
|
+
throw new Error(`Unrecognized option${keys.length > 1 ? 's' : ''} specified for ${packageName}: ${keys.join(', ')}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.on('uiLoaded', async ({playbook, uiCatalog}) => {
|
|
23
|
+
playbook.env.SITE_LISTING_EXTENSION_ENABLED = 'true'
|
|
24
|
+
const uiOutputDir = playbook.ui.outputDir
|
|
25
|
+
assetFile(uiCatalog, logger, uiOutputDir, 'css', 'listing.css')
|
|
26
|
+
const helperPath = 'helpers/listing-get-sections.js'
|
|
27
|
+
if (uiCatalog.findByType('helper').some(({ path }) => path === helperPath)) {
|
|
28
|
+
// ignore
|
|
29
|
+
} else {
|
|
30
|
+
const helperFilePath = ospath.join(__dirname, '../data', helperPath)
|
|
31
|
+
uiCatalog.addFile({
|
|
32
|
+
contents: Buffer.from(template(await fsp.readFile(helperFilePath, 'utf8'), {listingConfig: JSON.stringify(listing)})),
|
|
33
|
+
path: helperPath,
|
|
34
|
+
stem: ospath.parse(helperPath).name,
|
|
35
|
+
type: 'helper',
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
await helperFile(uiCatalog, 'helpers/image-spec.js')
|
|
39
|
+
await helperFile(uiCatalog, 'helpers/resolve-resource-url.js')
|
|
40
|
+
await partialFile(uiCatalog, 'partials/listing.hbs')
|
|
41
|
+
await partialFile(uiCatalog, 'partials/listing-card.hbs')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.on('beforePublish', ({ playbook }) => {
|
|
45
|
+
delete playbook.env.SITE_LISTING_EXTENSION_ENABLED
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async function partialFile(uiCatalog, partialPath) {
|
|
50
|
+
if (uiCatalog.findByType('partial').some(({ path }) => path === partialPath)) {
|
|
51
|
+
// ignore
|
|
52
|
+
} else {
|
|
53
|
+
const partialFilePath = ospath.join(__dirname, '../data', partialPath)
|
|
54
|
+
uiCatalog.addFile({
|
|
55
|
+
contents: Buffer.from(await fsp.readFile(partialFilePath, 'utf8')),
|
|
56
|
+
path: partialPath,
|
|
57
|
+
stem: ospath.parse(partialPath).name,
|
|
58
|
+
type: 'partial',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function helperFile(uiCatalog, helperPath) {
|
|
64
|
+
if (uiCatalog.findByType('helper').some(({ path }) => path === helperPath)) {
|
|
65
|
+
// ignore
|
|
66
|
+
} else {
|
|
67
|
+
const helperFilePath = ospath.join(__dirname, '../data', helperPath)
|
|
68
|
+
uiCatalog.addFile({
|
|
69
|
+
contents: Buffer.from(await fsp.readFile(helperFilePath, 'utf8')),
|
|
70
|
+
path: helperPath,
|
|
71
|
+
stem: ospath.parse(helperPath).name,
|
|
72
|
+
type: 'helper',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assetFile (
|
|
78
|
+
uiCatalog,
|
|
79
|
+
logger,
|
|
80
|
+
uiOutputDir,
|
|
81
|
+
assetDir,
|
|
82
|
+
basename,
|
|
83
|
+
assetPath = assetDir + '/' + basename,
|
|
84
|
+
contents = new LazyReadable(() => fs.createReadStream(ospath.join(__dirname, '../data', assetPath))),
|
|
85
|
+
overwrite = false
|
|
86
|
+
) {
|
|
87
|
+
const outputDir = uiOutputDir + '/' + assetDir
|
|
88
|
+
const existingFile = uiCatalog.findByType('asset').some(({path}) => path === assetPath)
|
|
89
|
+
if (existingFile) {
|
|
90
|
+
if (overwrite) {
|
|
91
|
+
logger.warn(`Please remove the following file from your UI since it is managed by ${packageName}: ${assetPath}`)
|
|
92
|
+
existingFile.contents = contents
|
|
93
|
+
delete existingFile.stat
|
|
94
|
+
} else {
|
|
95
|
+
logger.info(`The following file already exists in your UI: ${assetPath}, skipping`)
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
uiCatalog.addFile({
|
|
99
|
+
contents,
|
|
100
|
+
type: 'asset',
|
|
101
|
+
path: assetPath,
|
|
102
|
+
out: {dirname: outputDir, path: outputDir + '/' + basename, basename},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports.register = register
|
package/src/lunr.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antora extension for automatic Lunr.js search index generation
|
|
3
|
+
*
|
|
4
|
+
* This extension automatically generates a Lunr.js compatible search index
|
|
5
|
+
* after the site is published. It extracts content from all HTML files
|
|
6
|
+
* and creates a JSON index that can be used for client-side search.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const path = require('path')
|
|
11
|
+
|
|
12
|
+
// Lazy load JSDOM only when needed
|
|
13
|
+
let JSDOM
|
|
14
|
+
function loadJSDOM() {
|
|
15
|
+
if (!JSDOM) {
|
|
16
|
+
try {
|
|
17
|
+
JSDOM = require('jsdom').JSDOM
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error('JSDOM is required for the Lunr extension. Install it with: npm install jsdom')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return JSDOM
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function register({ config, playbook }) {
|
|
26
|
+
const logger = this.getLogger('lunr-extension')
|
|
27
|
+
|
|
28
|
+
this.once('sitePublished', (event) => {
|
|
29
|
+
logger.info('Generating Lunr.js search index...')
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Get output directory from the playbook
|
|
33
|
+
const outputDir = playbook.output?.dir || 'build/site'
|
|
34
|
+
logger.info(`Output directory: ${outputDir}`)
|
|
35
|
+
|
|
36
|
+
generateSearchIndex(outputDir, config.lunr || {}, logger)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error('Failed to generate search index:', error)
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateSearchIndex(outputDir, config = {}, logger) {
|
|
45
|
+
const searchIndexFile = path.join(outputDir, config.indexFile || 'search-index.json')
|
|
46
|
+
const maxContentLength = config.maxContentLength || 1000
|
|
47
|
+
const minContentLength = config.minContentLength || 50
|
|
48
|
+
|
|
49
|
+
// Find all HTML files
|
|
50
|
+
const htmlFiles = walkDirectory(outputDir).filter(file => file.endsWith('.html'))
|
|
51
|
+
const documents = []
|
|
52
|
+
let id = 0
|
|
53
|
+
|
|
54
|
+
htmlFiles.forEach(filePath => {
|
|
55
|
+
try {
|
|
56
|
+
const html = fs.readFileSync(filePath, 'utf8')
|
|
57
|
+
const title = getTitle(html)
|
|
58
|
+
const content = extractTextContent(html)
|
|
59
|
+
|
|
60
|
+
// Skip empty content
|
|
61
|
+
if (!content || content.length < minContentLength) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Generate relative URL
|
|
66
|
+
const relativePath = path.relative(outputDir, filePath)
|
|
67
|
+
const url = '/' + relativePath.replace(/\\/g, '/').replace(/\/index\.html$/, '/')
|
|
68
|
+
|
|
69
|
+
documents.push({
|
|
70
|
+
id: ++id,
|
|
71
|
+
title: title,
|
|
72
|
+
content: content.substring(0, maxContentLength),
|
|
73
|
+
url: url
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (config.debug) {
|
|
77
|
+
logger.debug(`Indexed: ${title} (${url})`)
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.warn(`Failed to process ${filePath}:`, error.message)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const searchIndex = {
|
|
85
|
+
documents: documents
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Write search index
|
|
89
|
+
fs.writeFileSync(searchIndexFile, JSON.stringify(searchIndex, null, 2))
|
|
90
|
+
logger.info(`Search index generated: ${documents.length} documents → ${searchIndexFile}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractTextContent(html) {
|
|
94
|
+
try {
|
|
95
|
+
const JSDOM = loadJSDOM()
|
|
96
|
+
const dom = new JSDOM(html)
|
|
97
|
+
const document = dom.window.document
|
|
98
|
+
|
|
99
|
+
// Remove script and style elements
|
|
100
|
+
const scripts = document.querySelectorAll('script, style, nav, .navbar, .footer')
|
|
101
|
+
scripts.forEach(el => el.remove())
|
|
102
|
+
|
|
103
|
+
// Get main content area
|
|
104
|
+
const main = document.querySelector('main, .main, .content, article')
|
|
105
|
+
const content = main || document.body
|
|
106
|
+
|
|
107
|
+
return content.textContent.trim().replace(/\s+/g, ' ')
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return ''
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getTitle(html) {
|
|
114
|
+
try {
|
|
115
|
+
const JSDOM = loadJSDOM()
|
|
116
|
+
const dom = new JSDOM(html)
|
|
117
|
+
const titleEl = dom.window.document.querySelector('title, h1, .page-title')
|
|
118
|
+
return titleEl ? titleEl.textContent.trim() : 'Untitled'
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return 'Untitled'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function walkDirectory(dir, fileList = []) {
|
|
125
|
+
if (!fs.existsSync(dir)) {
|
|
126
|
+
return fileList
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const files = fs.readdirSync(dir)
|
|
130
|
+
|
|
131
|
+
files.forEach(file => {
|
|
132
|
+
const filePath = path.join(dir, file)
|
|
133
|
+
const stat = fs.statSync(filePath)
|
|
134
|
+
|
|
135
|
+
if (stat.isDirectory()) {
|
|
136
|
+
walkDirectory(filePath, fileList)
|
|
137
|
+
} else {
|
|
138
|
+
fileList.push(filePath)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return fileList
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { register }
|
package/src/template.js
ADDED
|
@@ -16,9 +16,9 @@ function getToolboxPageModules(contentCatalog, version, tag) {
|
|
|
16
16
|
}, {})
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const logger =
|
|
21
|
-
|
|
19
|
+
function register() {
|
|
20
|
+
const logger = this.getLogger('toolbox-navigation')
|
|
21
|
+
this.on('navigationBuilt', ({contentCatalog, navigationCatalog}) => {
|
|
22
22
|
const toolboxComponent = contentCatalog.getComponent('toolboxes')
|
|
23
23
|
if (toolboxComponent) {
|
|
24
24
|
const componentVersions = toolboxComponent.versions
|
|
@@ -91,3 +91,5 @@ module.exports.register = (context) => {
|
|
|
91
91
|
}
|
|
92
92
|
})
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
module.exports.register = register
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { describe, it } = require('mocha')
|
|
2
|
+
const { expect } = require('chai')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
describe('Lunr extension', () => {
|
|
7
|
+
const { register } = require('../src/lunr.js')
|
|
8
|
+
|
|
9
|
+
it('should export a register function', () => {
|
|
10
|
+
expect(register).to.be.a('function')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should register with Antora context', () => {
|
|
14
|
+
let eventRegistered = false
|
|
15
|
+
const mockContext = {
|
|
16
|
+
getLogger: () => ({
|
|
17
|
+
info: () => {},
|
|
18
|
+
error: () => {},
|
|
19
|
+
debug: () => {},
|
|
20
|
+
warn: () => {}
|
|
21
|
+
}),
|
|
22
|
+
once: (event, handler) => {
|
|
23
|
+
if (event === 'sitePublished') {
|
|
24
|
+
eventRegistered = true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const config = {}
|
|
30
|
+
const playbook = { output: { dir: 'build/site' } }
|
|
31
|
+
|
|
32
|
+
register.call(mockContext, { config, playbook })
|
|
33
|
+
|
|
34
|
+
expect(eventRegistered).to.be.true
|
|
35
|
+
})
|
|
36
|
+
})
|