@druck-editorial/engine 0.1.2 → 0.2.1
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/dist/frontpage.d.ts +12 -1
- package/dist/frontpage.js +215 -8
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/frontpage.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { ArticleData, RenderOptions } from './types.js';
|
|
2
2
|
export type FrontPageRowType = 'hero' | 'feature' | 'triple' | 'brief';
|
|
3
|
+
export interface FrontPageItem extends ArticleData {
|
|
4
|
+
role: 'lead' | 'feature' | 'brief';
|
|
5
|
+
hasImage: boolean;
|
|
6
|
+
}
|
|
3
7
|
export interface FrontPageRow {
|
|
4
8
|
type: FrontPageRowType;
|
|
5
|
-
items:
|
|
9
|
+
items: FrontPageItem[];
|
|
6
10
|
}
|
|
7
11
|
export declare function buildFrontPage(items: ArticleData[]): FrontPageRow[];
|
|
12
|
+
export type FrontPageComposer = (rows: FrontPageRow[], opts?: RenderOptions) => string;
|
|
13
|
+
export interface FrontPagePartition {
|
|
14
|
+
lead?: FrontPageItem;
|
|
15
|
+
cells: FrontPageItem[];
|
|
16
|
+
brief: FrontPageItem[];
|
|
17
|
+
}
|
|
18
|
+
export declare function partitionRows(rows: FrontPageRow[]): FrontPagePartition;
|
|
8
19
|
export declare function renderFrontPage(rows: FrontPageRow[], opts?: RenderOptions): string;
|
package/dist/frontpage.js
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
import { escapeHtml, safeUrl } from './format.js';
|
|
4
4
|
import { categoryClass, renderCard } from './render.js';
|
|
5
5
|
const BRIEF_MAX = 5;
|
|
6
|
+
const ROLE_BY_TYPE = {
|
|
7
|
+
hero: 'lead',
|
|
8
|
+
feature: 'feature',
|
|
9
|
+
triple: 'feature',
|
|
10
|
+
brief: 'brief',
|
|
11
|
+
};
|
|
12
|
+
function enrichItem(data, role) {
|
|
13
|
+
return { ...data, role, hasImage: Boolean(safeUrl(data.heroImage)) };
|
|
14
|
+
}
|
|
6
15
|
export function buildFrontPage(items) {
|
|
7
16
|
if (!items.length)
|
|
8
17
|
return [];
|
|
@@ -10,15 +19,20 @@ export function buildFrontPage(items) {
|
|
|
10
19
|
const hotIdx = pool.findIndex((entry) => entry.hot);
|
|
11
20
|
if (hotIdx > 0)
|
|
12
21
|
pool.unshift(pool.splice(hotIdx, 1)[0]);
|
|
13
|
-
const
|
|
22
|
+
const raw = [
|
|
23
|
+
{ type: 'hero', items: pool.splice(0, 1) },
|
|
24
|
+
];
|
|
14
25
|
if (pool.length >= 2)
|
|
15
|
-
|
|
26
|
+
raw.push({ type: 'feature', items: pool.splice(0, 2) });
|
|
16
27
|
if (pool.length >= 3)
|
|
17
|
-
|
|
28
|
+
raw.push({ type: 'triple', items: pool.splice(0, 3) });
|
|
18
29
|
const brief = pool.splice(0, BRIEF_MAX);
|
|
19
30
|
if (brief.length)
|
|
20
|
-
|
|
21
|
-
return
|
|
31
|
+
raw.push({ type: 'brief', items: brief });
|
|
32
|
+
return raw.map((row) => ({
|
|
33
|
+
type: row.type,
|
|
34
|
+
items: row.items.map((data) => enrichItem(data, ROLE_BY_TYPE[row.type])),
|
|
35
|
+
}));
|
|
22
36
|
}
|
|
23
37
|
function renderHeroCard(data) {
|
|
24
38
|
const href = safeUrl(data.shareUrl ?? '') || '#';
|
|
@@ -40,8 +54,9 @@ function renderBriefItem(data) {
|
|
|
40
54
|
`<time>${escapeHtml(data.publishedAt)}</time>` +
|
|
41
55
|
'</a></li>');
|
|
42
56
|
}
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
function composeClassic(rows, opts) {
|
|
58
|
+
return rows
|
|
59
|
+
.map((row) => {
|
|
45
60
|
if (row.type === 'hero') {
|
|
46
61
|
return `<div class="df-row df-row--hero">${renderHeroCard(row.items[0])}</div>`;
|
|
47
62
|
}
|
|
@@ -52,6 +67,198 @@ export function renderFrontPage(rows, opts) {
|
|
|
52
67
|
const cls = row.type === 'feature' ? 'df-row--feature' : 'df-row--triple';
|
|
53
68
|
const cards = row.items.map((entry) => renderCard(entry, opts)).join('');
|
|
54
69
|
return `<div class="df-row ${cls}">${cards}</div>`;
|
|
70
|
+
})
|
|
71
|
+
.join('');
|
|
72
|
+
}
|
|
73
|
+
export function partitionRows(rows) {
|
|
74
|
+
return {
|
|
75
|
+
lead: rows.find((r) => r.type === 'hero')?.items[0],
|
|
76
|
+
cells: rows.filter((r) => r.type === 'feature' || r.type === 'triple').flatMap((r) => r.items),
|
|
77
|
+
brief: rows.find((r) => r.type === 'brief')?.items ?? [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function safeHref(item) {
|
|
81
|
+
return escapeHtml(safeUrl(item.shareUrl ?? '') || '#');
|
|
82
|
+
}
|
|
83
|
+
function safeImg(item) {
|
|
84
|
+
return escapeHtml(safeUrl(item.heroImage) || 'data:,');
|
|
85
|
+
}
|
|
86
|
+
function brutalistLead(item) {
|
|
87
|
+
const kicker = `${item.category}${item.hot ? ' / Hot' : ''}`;
|
|
88
|
+
return ('<div class="dfb-lead">' +
|
|
89
|
+
'<div class="dfb-head">' +
|
|
90
|
+
`<span class="dfb-kicker">${escapeHtml(kicker)}</span>` +
|
|
91
|
+
`<a class="dfb-title" href="${safeHref(item)}"><h2>${escapeHtml(item.title)}</h2></a>` +
|
|
92
|
+
'</div>' +
|
|
93
|
+
`<a class="dfb-img" href="${safeHref(item)}">` +
|
|
94
|
+
`<img src="${safeImg(item)}" alt="${escapeHtml(item.heroImageAlt ?? item.title)}" loading="lazy" width="1200" height="675">` +
|
|
95
|
+
'</a>' +
|
|
96
|
+
'</div>');
|
|
97
|
+
}
|
|
98
|
+
function brutalistCell(item, n) {
|
|
99
|
+
return (`<a class="dfb-cell" href="${safeHref(item)}">` +
|
|
100
|
+
`<span class="dfb-n">${String(n).padStart(2, '0')}</span>` +
|
|
101
|
+
`<span class="dfb-ck">${escapeHtml(item.category)}</span>` +
|
|
102
|
+
`<h3>${escapeHtml(item.title)}</h3>` +
|
|
103
|
+
'</a>');
|
|
104
|
+
}
|
|
105
|
+
function brutalistBriefItem(item) {
|
|
106
|
+
return (`<li><a href="${safeHref(item)}">` +
|
|
107
|
+
`<span class="dfb-bt">${escapeHtml(item.title)}</span>` +
|
|
108
|
+
`<time>${escapeHtml(item.publishedAt)}</time>` +
|
|
109
|
+
'</a></li>');
|
|
110
|
+
}
|
|
111
|
+
function composeBrutalist(rows, _opts) {
|
|
112
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
113
|
+
const parts = [
|
|
114
|
+
'<div class="dfb-mast"><span class="dfb-wm">Druck</span></div><div class="dfb-rule"></div>',
|
|
115
|
+
];
|
|
116
|
+
if (lead)
|
|
117
|
+
parts.push(brutalistLead(lead));
|
|
118
|
+
if (cells.length) {
|
|
119
|
+
parts.push(`<div class="dfb-grid">${cells.map((c, i) => brutalistCell(c, i + 2)).join('')}</div>`);
|
|
120
|
+
}
|
|
121
|
+
if (brief.length) {
|
|
122
|
+
parts.push(`<ol class="dfb-brief">${brief.map(brutalistBriefItem).join('')}</ol>`);
|
|
123
|
+
}
|
|
124
|
+
return parts.join('');
|
|
125
|
+
}
|
|
126
|
+
function composeBroadsheet(rows, _opts) {
|
|
127
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
128
|
+
const parts = ['<div class="dfbr-mast">The Druck</div><div class="dfbr-rule"></div>'];
|
|
129
|
+
if (lead) {
|
|
130
|
+
parts.push(`<div class="dfbr-date">${escapeHtml(lead.publishedAt)}</div>`);
|
|
131
|
+
parts.push(`<a class="dfbr-leadtitle" href="${safeHref(lead)}"><h2>${escapeHtml(lead.title)}</h2></a>`);
|
|
132
|
+
parts.push(`<p class="dfbr-sub">${escapeHtml(lead.subtitle)}</p>`);
|
|
133
|
+
}
|
|
134
|
+
const stories = [...cells, ...brief];
|
|
135
|
+
if (stories.length) {
|
|
136
|
+
parts.push('<div class="dfbr-cols">' + stories.map((s, i) => `<div class="dfbr-story${i === 0 ? ' dfbr-drop' : ''}"><h3>${escapeHtml(s.category)}</h3>` +
|
|
137
|
+
`<a href="${safeHref(s)}"><b>${escapeHtml(s.title)}</b></a> ${escapeHtml(s.subtitle)}</div>`).join('') + '</div>');
|
|
138
|
+
}
|
|
139
|
+
return parts.join('');
|
|
140
|
+
}
|
|
141
|
+
function composeHeroBlocks(rows, prefix) {
|
|
142
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (lead) {
|
|
145
|
+
parts.push(`<a class="${prefix}-hero" href="${safeHref(lead)}">` +
|
|
146
|
+
`<img src="${safeImg(lead)}" alt="${escapeHtml(lead.heroImageAlt ?? lead.title)}" loading="lazy" width="1600" height="900">` +
|
|
147
|
+
`<span class="${prefix}-scrim"></span>` +
|
|
148
|
+
`<span class="${prefix}-htext"><span class="${prefix}-kick">${escapeHtml(lead.category)}</span>` +
|
|
149
|
+
`<h2>${escapeHtml(lead.title)}</h2></span></a>`);
|
|
150
|
+
}
|
|
151
|
+
const stories = [...cells, ...brief].slice(0, 6);
|
|
152
|
+
if (stories.length) {
|
|
153
|
+
parts.push(`<div class="${prefix}-body">` + stories.map((s) => `<a class="${prefix}-story" href="${safeHref(s)}"><span class="${prefix}-k">${escapeHtml(s.category)}</span>` +
|
|
154
|
+
`<h3>${escapeHtml(s.title)}</h3><p>${escapeHtml(s.subtitle)}</p></a>`).join('') + '</div>');
|
|
155
|
+
}
|
|
156
|
+
return parts.join('');
|
|
157
|
+
}
|
|
158
|
+
function composeLuxury(rows, _opts) {
|
|
159
|
+
return composeHeroBlocks(rows, 'dflx');
|
|
160
|
+
}
|
|
161
|
+
function composeNoir(rows, _opts) {
|
|
162
|
+
return composeHeroBlocks(rows, 'dfnr');
|
|
163
|
+
}
|
|
164
|
+
function composeBento(rows, _opts) {
|
|
165
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
166
|
+
const tiles = [];
|
|
167
|
+
if (lead) {
|
|
168
|
+
tiles.push(`<a class="dfbn-tile dfbn-hero" href="${safeHref(lead)}">` +
|
|
169
|
+
`<img src="${safeImg(lead)}" alt="${escapeHtml(lead.heroImageAlt ?? lead.title)}" loading="lazy" width="1200" height="800">` +
|
|
170
|
+
'<span class="dfbn-ov"></span>' +
|
|
171
|
+
`<span class="dfbn-tag">${escapeHtml(lead.category)}</span>` +
|
|
172
|
+
`<h3>${escapeHtml(lead.title)}</h3></a>`);
|
|
173
|
+
}
|
|
174
|
+
cells.forEach((c) => {
|
|
175
|
+
const media = c.hasImage
|
|
176
|
+
? `<img src="${safeImg(c)}" alt="${escapeHtml(c.heroImageAlt ?? c.title)}" loading="lazy" width="800" height="600"><span class="dfbn-ov"></span>`
|
|
177
|
+
: '';
|
|
178
|
+
tiles.push(`<a class="dfbn-tile${c.hasImage ? ' dfbn-img' : ''}" href="${safeHref(c)}">${media}` +
|
|
179
|
+
`<span class="dfbn-tag">${escapeHtml(c.category)}</span>` +
|
|
180
|
+
`<h4>${escapeHtml(c.title)}</h4></a>`);
|
|
181
|
+
});
|
|
182
|
+
brief.slice(0, 3).forEach((b) => {
|
|
183
|
+
tiles.push(`<a class="dfbn-tile dfbn-mini" href="${safeHref(b)}"><span class="dfbn-tag">${escapeHtml(b.category)}</span>` +
|
|
184
|
+
`<h4>${escapeHtml(b.title)}</h4></a>`);
|
|
55
185
|
});
|
|
56
|
-
return `<div class="
|
|
186
|
+
return `<div class="dfbn-head"><span class="dfbn-wm">Druck</span></div><div class="dfbn-grid">${tiles.join('')}</div>`;
|
|
187
|
+
}
|
|
188
|
+
function composeBloomberg(rows, _opts) {
|
|
189
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
190
|
+
const row = (item, isLead = false) => `<a class="dfbb-row${isLead ? ' dfbb-lead' : ''}" href="${safeHref(item)}">` +
|
|
191
|
+
`<span class="dfbb-c${isLead && item.hot ? ' dfbb-hot' : ''}">${escapeHtml(isLead && item.hot ? 'HOT' : item.category)}</span>` +
|
|
192
|
+
`<span class="dfbb-t">${escapeHtml(item.title)}</span>` +
|
|
193
|
+
`<time class="dfbb-time">${escapeHtml(item.publishedAt)}</time></a>`;
|
|
194
|
+
const parts = [
|
|
195
|
+
'<div class="dfbb-top"><span>DRUCK TERMINAL</span><span>TOP STORIES</span></div>',
|
|
196
|
+
];
|
|
197
|
+
if (lead)
|
|
198
|
+
parts.push(row(lead, true));
|
|
199
|
+
parts.push([...cells, ...brief].map((s) => row(s)).join(''));
|
|
200
|
+
return `<div class="dfbb-inner">${parts.join('')}</div>`;
|
|
201
|
+
}
|
|
202
|
+
function composeBauhaus(rows, _opts) {
|
|
203
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
204
|
+
const inner = [];
|
|
205
|
+
if (lead) {
|
|
206
|
+
inner.push(`<a class="dfbh-hero" href="${safeHref(lead)}">` +
|
|
207
|
+
`<span class="dfbh-art"><img src="${safeImg(lead)}" alt="${escapeHtml(lead.heroImageAlt ?? lead.title)}" loading="lazy" width="1200" height="800"></span>` +
|
|
208
|
+
`<span class="dfbh-plate"><span class="dfbh-k">${escapeHtml(lead.category)}</span>` +
|
|
209
|
+
`<h2>${escapeHtml(lead.title)}</h2><span class="dfbh-sub">${escapeHtml(lead.subtitle)}</span></span></a>`);
|
|
210
|
+
}
|
|
211
|
+
const stories = [...cells, ...brief];
|
|
212
|
+
if (stories.length) {
|
|
213
|
+
inner.push('<ul class="dfbh-list">' +
|
|
214
|
+
stories.map((s) => `<li><a href="${safeHref(s)}"><span class="dfbh-lk">${escapeHtml(s.category)}</span>${escapeHtml(s.title)}</a></li>`).join('') +
|
|
215
|
+
'</ul>');
|
|
216
|
+
}
|
|
217
|
+
return ('<span class="dfbh-sq"></span><span class="dfbh-circ"></span><span class="dfbh-tri"></span><span class="dfbh-bar"></span>' +
|
|
218
|
+
`<div class="dfbh-inner">${inner.join('')}</div>`);
|
|
219
|
+
}
|
|
220
|
+
function composeTabloid(rows, _opts) {
|
|
221
|
+
const { lead, cells, brief } = partitionRows(rows);
|
|
222
|
+
const parts = [
|
|
223
|
+
'<div class="dftb-mast"><span class="dftb-wm">Druck</span><span class="dftb-strap">The People's Front Page</span></div>',
|
|
224
|
+
];
|
|
225
|
+
const inner = [];
|
|
226
|
+
if (lead) {
|
|
227
|
+
inner.push(`<a class="dftb-splash" href="${safeHref(lead)}">` +
|
|
228
|
+
(lead.hasImage
|
|
229
|
+
? `<span class="dftb-art"><img src="${safeImg(lead)}" alt="${escapeHtml(lead.heroImageAlt ?? lead.title)}" loading="lazy" width="1200" height="800"></span>`
|
|
230
|
+
: '') +
|
|
231
|
+
'<span class="dftb-splash-text"><span class="dftb-k">Exclusive</span>' +
|
|
232
|
+
`<h2>${escapeHtml(lead.title)}</h2><span class="dftb-deck">${escapeHtml(lead.subtitle)}</span></span></a>`);
|
|
233
|
+
}
|
|
234
|
+
if (cells.length) {
|
|
235
|
+
inner.push('<div class="dftb-shouts">' +
|
|
236
|
+
cells.map((c) => `<a class="dftb-shout" href="${safeHref(c)}"><span class="dftb-sk">${escapeHtml(c.category)}</span><h3>${escapeHtml(c.title)}</h3></a>`).join('') +
|
|
237
|
+
'</div>');
|
|
238
|
+
}
|
|
239
|
+
if (brief.length) {
|
|
240
|
+
inner.push('<div class="dftb-more"><span class="dftb-more-label">More inside</span><ul class="dftb-list">' +
|
|
241
|
+
brief.map((b) => `<li><a href="${safeHref(b)}">${escapeHtml(b.title)}</a></li>`).join('') +
|
|
242
|
+
'</ul></div>');
|
|
243
|
+
}
|
|
244
|
+
parts.push(`<div class="dftb-inner">${inner.join('')}</div>`);
|
|
245
|
+
return parts.join('');
|
|
246
|
+
}
|
|
247
|
+
const COMPOSERS = {
|
|
248
|
+
classic: composeClassic,
|
|
249
|
+
brutalist: composeBrutalist,
|
|
250
|
+
broadsheet: composeBroadsheet,
|
|
251
|
+
luxury: composeLuxury,
|
|
252
|
+
noir: composeNoir,
|
|
253
|
+
bento: composeBento,
|
|
254
|
+
bloomberg: composeBloomberg,
|
|
255
|
+
bauhaus: composeBauhaus,
|
|
256
|
+
tabloid: composeTabloid,
|
|
257
|
+
};
|
|
258
|
+
export function renderFrontPage(rows, opts) {
|
|
259
|
+
const requested = opts?.look ?? 'classic';
|
|
260
|
+
const compose = COMPOSERS[requested] ?? composeClassic;
|
|
261
|
+
const look = COMPOSERS[requested] ? requested : 'classic';
|
|
262
|
+
const lookClass = look === 'classic' ? '' : ` druck-front-page--${look}`;
|
|
263
|
+
return `<div class="druck-front-page${lookClass}">${compose(rows, opts)}</div>`;
|
|
57
264
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { ArticleFormat, ArticleData, Category, Chapter, KeyPoint, StatAside, SourceQuote, Figure, KnowCard, RelatedArticle, WeeklyData, WeeklySection, WeeklySectionArticle, RenderOptions, } from './types.js';
|
|
1
|
+
export type { ArticleFormat, ArticleData, Category, Chapter, KeyPoint, StatAside, SourceQuote, Figure, KnowCard, RelatedArticle, WeeklyData, WeeklySection, WeeklySectionArticle, RenderOptions, FrontPageLook, } from './types.js';
|
|
2
2
|
export { renderArticle, renderCard } from './render.js';
|
|
3
3
|
export { transformInlineBlocks, sanitizeInline, escapeHtml, safeUrl } from './format.js';
|
|
4
4
|
export { buildFrontPage, renderFrontPage } from './frontpage.js';
|
|
5
|
-
export type { FrontPageRow, FrontPageRowType } from './frontpage.js';
|
|
5
|
+
export type { FrontPageRow, FrontPageRowType, FrontPageItem } from './frontpage.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export interface WeeklySectionArticle {
|
|
|
99
99
|
image?: string;
|
|
100
100
|
summary?: string;
|
|
101
101
|
}
|
|
102
|
+
export type FrontPageLook = 'classic' | 'broadsheet' | 'brutalist' | 'luxury' | 'noir' | 'bento' | 'bloomberg' | 'bauhaus' | 'tabloid';
|
|
102
103
|
export interface RenderOptions {
|
|
103
104
|
lang?: string;
|
|
104
105
|
theme?: 'light' | 'dark';
|
|
@@ -107,4 +108,5 @@ export interface RenderOptions {
|
|
|
107
108
|
siteUrl?: string;
|
|
108
109
|
canonicalUrl?: string;
|
|
109
110
|
ogImageUrl?: string;
|
|
111
|
+
look?: FrontPageLook;
|
|
110
112
|
}
|
package/package.json
CHANGED