-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
190 lines (157 loc) · 6.45 KB
/
Copy pathmain.js
File metadata and controls
190 lines (157 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Series Navigator - Copyright (C) 2026 InPoint Automation Sp. z o.o.
// Licensed under the GNU General Public License v3 or later; see LICENSE.
//
// Groups posts into ordered series and injects a TOC + "next" link into each post.
class SeriesNavigator {
constructor(API, name, config) {
this.API = API;
this.name = name;
this.config = config;
this.seriesData = null;
this.postSeriesLookup = {};
try {
this.seriesData = JSON.parse(this.config.seriesDataJson || '{"series":[]}');
} catch (e) {
console.warn('[Series Navigator] Failed to parse series data:', e.message);
this.seriesData = { series: [] };
}
}
addInsertions() {
this.API.addInsertion('customHeadCode', this.addStyles, 1, this);
}
addModifiers() {
// htmlOutput carries the post context as its 4th arg
this.API.addModifier('htmlOutput', this.injectSeriesNav, 1, this);
}
addEvents() {
this.API.addEvent('beforeRender', this.buildLookup, 1, this);
}
// beforeRender: slug -> series-info map
buildLookup(rendererInstance) {
if (!this.seriesData || !this.seriesData.series.length) return;
for (let s = 0; s < this.seriesData.series.length; s++) {
const series = this.seriesData.series[s];
const total = series.posts.length;
for (let i = 0; i < total; i++) {
const post = series.posts[i];
const prev = i > 0 ? series.posts[i - 1] : null;
const next = i < total - 1 ? series.posts[i + 1] : null;
this.postSeriesLookup[post.slug] = {
seriesName: series.name,
seriesId: series.id,
index: i,
total: total,
partNumber: i + 1,
posts: series.posts,
prev: prev,
next: next
};
}
}
console.log('[Series Navigator] Built lookup for ' + Object.keys(this.postSeriesLookup).length + ' posts across ' + this.seriesData.series.length + ' series.');
}
// Post URL exactly as Publii builds it
buildPostUrl(rendererInstance, slug) {
const cfg = rendererInstance.siteConfig;
const urls = cfg.advanced.urls;
const prefix = urls.postsPrefix ? urls.postsPrefix + '/' : '';
const base = cfg.domain.replace(/\/$/, '') + '/' + prefix + slug;
if (urls.cleanUrls) {
let url = base + '/';
if (rendererInstance.previewMode || urls.addIndex) url += 'index.html';
return url;
}
return base + '.html';
}
// customHeadCode: styles only on pages that are in a series
addStyles(rendererInstance, context) {
if (!context || !context.post) return '';
if (!this.postSeriesLookup[context.post.slug]) return '';
return '<style>'
+ '.project-nav li.is-current a {'
+ ' font-weight: bold;'
+ ' text-decoration: none;'
+ '}'
+ '.project-nav__next {'
+ ' margin-top: 2em;'
+ '}'
+ '</style>';
}
// TOC at top of content, next-link at bottom.
injectSeriesNav(rendererInstance, html, globalContext, context) {
if (!this.seriesData || !this.seriesData.series.length) return html;
if (!context || !context.post) return html; // post pages only
const slug = context.post.slug;
const info = this.postSeriesLookup[slug];
if (!info) return html;
const showPartNumbers = this.config.showPartNumbers;
const heading = this.config.seriesHeading || 'In this series';
const nextPrefix = this.config.nextLinkText || 'Next:';
let tocItems = '';
for (let i = 0; i < info.posts.length; i++) {
const post = info.posts[i];
const isCurrent = post.slug === slug;
const partPrefix = showPartNumbers ? 'Part ' + (i + 1) + ': ' : '';
const url = this.buildPostUrl(rendererInstance, post.slug);
const cls = isCurrent ? ' class="is-current"' : '';
tocItems += '<li' + cls + '><a href="' + url + '">' + partPrefix + this.escHtml(post.title) + '</a></li>';
}
const tocHtml = '<nav class="project-nav">'
+ '<p class="project-nav__heading">' + this.escHtml(heading) + '</p>'
+ '<ul>' + tocItems + '</ul>'
+ '</nav>';
let nextHtml = '';
if (info.next) {
const nextUrl = this.buildPostUrl(rendererInstance, info.next.slug);
nextHtml = '<div class="project-nav__next">'
+ '<strong>' + this.escHtml(nextPrefix) + '</strong> <a href="' + nextUrl + '">' + this.escHtml(info.next.title) + '</a>'
+ '</div>';
}
// theme content-wrapper class strings
const wrapperPatterns = [
'class="entry-wrapper content__entry"',
'class="post__inner post__entry"'
];
let wrapperPos = -1;
for (let p = 0; p < wrapperPatterns.length; p++) {
wrapperPos = html.indexOf(wrapperPatterns[p]);
if (wrapperPos !== -1) break;
}
if (wrapperPos === -1) return html;
const contentStart = html.indexOf('>', wrapperPos) + 1;
let depth = 1;
let pos = contentStart;
let contentEnd = -1;
while (depth > 0 && pos < html.length) {
const nextOpen = html.indexOf('<div', pos);
const nextClose = html.indexOf('</div>', pos);
if (nextClose === -1) break;
if (nextOpen !== -1 && nextOpen < nextClose) {
depth++;
pos = nextOpen + 4;
} else {
depth--;
if (depth === 0) {
contentEnd = nextClose;
}
pos = nextClose + 6;
}
}
if (contentEnd === -1) return html;
// TOC after opening tag, next-link before closing div
html = html.slice(0, contentStart)
+ tocHtml
+ html.slice(contentStart, contentEnd)
+ nextHtml
+ html.slice(contentEnd);
return html;
}
escHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}
module.exports = SeriesNavigator;