@forwardimpact/libsyntheticrender 0.1.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.
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Link Validator — validates IRI consistency and link density.
3
+ *
4
+ * Checks that generated HTML files contain valid cross-file IRI links
5
+ * and that every entity meets minimum link density requirements.
6
+ *
7
+ * @module libuniverse/render/validate-links
8
+ */
9
+
10
+ /**
11
+ * Validate linked entities for IRI consistency and density.
12
+ * @param {import('./link-assigner.js').LinkedEntities} linked
13
+ * @param {string} domain
14
+ * @returns {{ passed: boolean, checks: object[] }}
15
+ */
16
+ export function validateLinks(linked, domain) {
17
+ const checks = [
18
+ checkIriNamespace(linked, domain),
19
+ checkProjectLinkDensity(linked),
20
+ checkCourseLinkDensity(linked),
21
+ checkEventLinkDensity(linked),
22
+ checkBlogLinkDensity(linked),
23
+ checkPlatformDAG(linked),
24
+ checkDrugDerivatives(linked),
25
+ checkCoursePrerequisites(linked),
26
+ ];
27
+
28
+ const failures = checks.filter((c) => !c.passed);
29
+ return {
30
+ passed: failures.length === 0,
31
+ total: checks.length,
32
+ failures: failures.length,
33
+ checks,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Check all IRIs use the correct domain namespace.
39
+ */
40
+ function checkIriNamespace(linked, domain) {
41
+ const prefix = `https://${domain}/id/`;
42
+ const errors = [];
43
+
44
+ for (const proj of linked.projects) {
45
+ if (!proj.iri.startsWith(prefix)) {
46
+ errors.push(`Project ${proj.id}: IRI ${proj.iri} doesn't match domain`);
47
+ }
48
+ }
49
+ for (const drug of linked.drugs) {
50
+ if (!drug.iri.startsWith(prefix)) {
51
+ errors.push(`Drug ${drug.id}: IRI ${drug.iri} doesn't match domain`);
52
+ }
53
+ }
54
+ for (const plat of linked.platforms) {
55
+ if (!plat.iri.startsWith(prefix)) {
56
+ errors.push(`Platform ${plat.id}: IRI ${plat.iri} doesn't match domain`);
57
+ }
58
+ }
59
+
60
+ return {
61
+ name: "iri_namespace",
62
+ passed: errors.length === 0,
63
+ message:
64
+ errors.length === 0
65
+ ? "All IRIs use consistent namespace"
66
+ : `${errors.length} IRI namespace violations: ${errors[0]}`,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Check every project has at least 2 cross-file links.
72
+ */
73
+ function checkProjectLinkDensity(linked) {
74
+ const sparse = linked.projects.filter((proj) => {
75
+ const linkCount =
76
+ (proj.drugLinks?.length || 0) +
77
+ (proj.platformLinks?.length || 0) +
78
+ (proj.members?.length || 0);
79
+ return linkCount < 2;
80
+ });
81
+
82
+ return {
83
+ name: "project_link_density",
84
+ passed: sparse.length === 0,
85
+ message:
86
+ sparse.length === 0
87
+ ? "All projects have ≥2 cross-file links"
88
+ : `${sparse.length} projects have <2 cross-file links`,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Check every course has at least 2 cross-file links.
94
+ */
95
+ function checkCourseLinkDensity(linked) {
96
+ const sparse = linked.courses.filter((course) => {
97
+ const linkCount =
98
+ (course.prerequisiteIris?.length || 0) +
99
+ (course.platformLink ? 1 : 0) +
100
+ (course.drugLink ? 1 : 0) +
101
+ (course.attendees?.length || 0);
102
+ return linkCount < 2;
103
+ });
104
+
105
+ return {
106
+ name: "course_link_density",
107
+ passed: sparse.length === 0,
108
+ message:
109
+ sparse.length === 0
110
+ ? "All courses have ≥2 cross-file links"
111
+ : `${sparse.length} courses have <2 cross-file links`,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Check every event has at least 2 cross-file links.
117
+ */
118
+ function checkEventLinkDensity(linked) {
119
+ const sparse = linked.events.filter((event) => {
120
+ const linkCount =
121
+ 1 + // organizer
122
+ (event.attendees?.length || 0) +
123
+ (event.aboutProjects?.length || 0) +
124
+ (event.aboutDrugs?.length || 0) +
125
+ (event.aboutPlatforms?.length || 0);
126
+ return linkCount < 2;
127
+ });
128
+
129
+ return {
130
+ name: "event_link_density",
131
+ passed: sparse.length === 0,
132
+ message:
133
+ sparse.length === 0
134
+ ? "All events have ≥2 cross-file links"
135
+ : `${sparse.length} events have <2 cross-file links`,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Check every blog post has at least 2 cross-file links.
141
+ */
142
+ function checkBlogLinkDensity(linked) {
143
+ const sparse = linked.blogPosts.filter((post) => {
144
+ const linkCount =
145
+ 1 + // author
146
+ (post.aboutDrugs?.length || 0) +
147
+ (post.aboutPlatforms?.length || 0) +
148
+ (post.aboutProjects?.length || 0) +
149
+ (post.mentionsPeople?.length || 0);
150
+ return linkCount < 2;
151
+ });
152
+
153
+ return {
154
+ name: "blog_link_density",
155
+ passed: sparse.length === 0,
156
+ message:
157
+ sparse.length === 0
158
+ ? "All blog posts have ≥2 cross-file links"
159
+ : `${sparse.length} blog posts have <2 cross-file links`,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Check platform dependencies form a DAG (no cycles).
165
+ */
166
+ function checkPlatformDAG(linked) {
167
+ const platformMap = new Map(linked.platforms.map((p) => [p.id, p]));
168
+ const visited = new Set();
169
+ const stack = new Set();
170
+ let cycle = null;
171
+
172
+ function dfs(id) {
173
+ if (stack.has(id)) {
174
+ cycle = id;
175
+ return true;
176
+ }
177
+ if (visited.has(id)) return false;
178
+ visited.add(id);
179
+ stack.add(id);
180
+
181
+ const plat = platformMap.get(id);
182
+ if (plat) {
183
+ const deps = (plat.dependencies || []).map((d) =>
184
+ typeof d === "string" ? d : d.id,
185
+ );
186
+ for (const dep of deps) {
187
+ if (dfs(dep)) return true;
188
+ }
189
+ }
190
+
191
+ stack.delete(id);
192
+ return false;
193
+ }
194
+
195
+ for (const plat of linked.platforms) {
196
+ if (dfs(plat.id)) break;
197
+ }
198
+
199
+ return {
200
+ name: "platform_dag",
201
+ passed: !cycle,
202
+ message: cycle
203
+ ? `Platform dependency cycle detected at: ${cycle}`
204
+ : "Platform dependencies form a valid DAG",
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Check drug derivative references point to valid parent drugs.
210
+ */
211
+ function checkDrugDerivatives(linked) {
212
+ const drugIds = new Set(linked.drugs.map((d) => d.id));
213
+ const errors = linked.drugs
214
+ .filter((d) => d.parentDrug && !drugIds.has(d.parentDrug))
215
+ .map((d) => `${d.id} references unknown parent ${d.parentDrug}`);
216
+
217
+ return {
218
+ name: "drug_derivatives",
219
+ passed: errors.length === 0,
220
+ message:
221
+ errors.length === 0
222
+ ? "All drug derivative references are valid"
223
+ : `${errors.length} invalid derivative references`,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Check course prerequisite references point to valid courses.
229
+ */
230
+ function checkCoursePrerequisites(linked) {
231
+ const courseIds = new Set(linked.courses.map((c) => c.id));
232
+ const errors = [];
233
+ for (const course of linked.courses) {
234
+ for (const prereq of course.prerequisites || []) {
235
+ if (!courseIds.has(prereq)) {
236
+ errors.push(`${course.id} references unknown prerequisite ${prereq}`);
237
+ }
238
+ }
239
+ }
240
+
241
+ return {
242
+ name: "course_prerequisites",
243
+ passed: errors.length === 0,
244
+ message:
245
+ errors.length === 0
246
+ ? "All course prerequisite references are valid"
247
+ : `${errors.length} invalid prerequisite references`,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Validate rendered HTML files for IRI consistency.
253
+ * @param {Map<string, string>} htmlFiles - filename → HTML content
254
+ * @param {string} domain
255
+ * @returns {{ passed: boolean, checks: object[] }}
256
+ */
257
+ export function validateHTML(htmlFiles, domain) {
258
+ const checks = [
259
+ checkEnrichedIriNamespace(htmlFiles, domain),
260
+ checkOrphanedLinks(htmlFiles),
261
+ ];
262
+
263
+ const failures = checks.filter((c) => !c.passed);
264
+ return {
265
+ passed: failures.length === 0,
266
+ total: checks.length,
267
+ failures: failures.length,
268
+ checks,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Scan all HTML for itemid values outside the universe domain.
274
+ */
275
+ function checkEnrichedIriNamespace(htmlFiles, domain) {
276
+ const errors = [];
277
+ const ITEMID_RE = /itemid="([^"]*)"/g;
278
+
279
+ for (const [filename, html] of htmlFiles) {
280
+ for (const match of html.matchAll(ITEMID_RE)) {
281
+ const iri = match[1];
282
+ if (!iri.startsWith(`https://${domain}/`)) {
283
+ errors.push(`${filename}: off-domain IRI ${iri}`);
284
+ }
285
+ }
286
+ }
287
+
288
+ return {
289
+ name: "enriched_iri_namespace",
290
+ passed: errors.length === 0,
291
+ message:
292
+ errors.length === 0
293
+ ? "All HTML itemid values use domain namespace"
294
+ : `${errors.length} off-domain IRIs: ${errors[0]}`,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Check that link href targets appear as itemid values somewhere in the corpus.
300
+ */
301
+ function checkOrphanedLinks(htmlFiles) {
302
+ const allItemIds = new Set();
303
+ const allHrefs = [];
304
+ const ITEMID_RE = /itemid="([^"]*)"/g;
305
+ const LINK_HREF_RE = /<link[^>]+href="([^"]*)"/g;
306
+
307
+ for (const [, html] of htmlFiles) {
308
+ for (const m of html.matchAll(ITEMID_RE)) allItemIds.add(m[1]);
309
+ }
310
+ for (const [filename, html] of htmlFiles) {
311
+ for (const m of html.matchAll(LINK_HREF_RE)) {
312
+ const href = m[1];
313
+ // Skip relative URLs — only check IRI references against itemid corpus
314
+ if (!href.startsWith("https://")) continue;
315
+ if (!allItemIds.has(href)) {
316
+ allHrefs.push(`${filename}: link href ${href} not found as any itemid`);
317
+ }
318
+ }
319
+ }
320
+
321
+ return {
322
+ name: "orphaned_links",
323
+ passed: allHrefs.length === 0,
324
+ message:
325
+ allHrefs.length === 0
326
+ ? "All link hrefs match existing itemid values"
327
+ : `${allHrefs.length} orphaned link hrefs: ${allHrefs[0]}`,
328
+ };
329
+ }
@@ -0,0 +1,31 @@
1
+ {{#articles}}
2
+ <article
3
+ itemscope
4
+ itemtype="https://schema.org/ScholarlyArticle"
5
+ itemid="{{{iri}}}"
6
+ >
7
+ <h2 itemprop="headline">{{title}}</h2>
8
+ <meta itemprop="identifier" content="{{identifier}}" />
9
+ <meta itemprop="keywords" content="{{keywords}}" />
10
+ <time itemprop="datePublished" datetime="{{date}}">{{date}}</time>
11
+
12
+ {{#author}}
13
+ <span
14
+ itemprop="author"
15
+ itemscope
16
+ itemtype="https://schema.org/Person"
17
+ itemid="{{{iri}}}"
18
+ >
19
+ <span itemprop="name">{{name}}</span>
20
+ </span>
21
+ {{/author}} {{#drugLinks}}
22
+ <link itemprop="about" href="{{{iri}}}" />
23
+ {{/drugLinks}} {{#platformLinks}}
24
+ <link itemprop="about" href="{{{iri}}}" />
25
+ {{/platformLinks}} {{#projectLinks}}
26
+ <link itemprop="about" href="{{{iri}}}" />
27
+ {{/projectLinks}}
28
+
29
+ <div itemprop="articleBody" data-enrich="article_{{topic}}">{{prose}}</div>
30
+ </article>
31
+ {{/articles}}
@@ -0,0 +1,34 @@
1
+ <section itemscope itemtype="https://schema.org/Blog" itemid="{{{blogIri}}}">
2
+ <span itemprop="name">Engineering Blog</span>
3
+ <article
4
+ itemprop="blogPost"
5
+ itemscope
6
+ itemtype="https://schema.org/BlogPosting"
7
+ itemid="{{{iri}}}"
8
+ >
9
+ <h2 itemprop="headline">{{headline}}</h2>
10
+ <meta itemprop="identifier" content="{{identifier}}" />
11
+ <meta itemprop="keywords" content="{{keywords}}" />
12
+ <time itemprop="datePublished" datetime="{{date}}">{{date}}</time>
13
+ <link itemprop="isPartOf" href="{{{blogIri}}}" />
14
+
15
+ {{#author}}
16
+ <span
17
+ itemprop="author"
18
+ itemscope
19
+ itemtype="https://schema.org/Person"
20
+ itemid="{{{iri}}}"
21
+ >
22
+ <span itemprop="name">{{name}}</span>
23
+ </span>
24
+ {{/author}} {{#aboutDrugs}}
25
+ <link itemprop="about" href="{{{iri}}}" />
26
+ {{/aboutDrugs}} {{#aboutPlatforms}}
27
+ <link itemprop="about" href="{{{iri}}}" />
28
+ {{/aboutPlatforms}} {{#aboutProjects}}
29
+ <link itemprop="about" href="{{{iri}}}" />
30
+ {{/aboutProjects}}
31
+
32
+ <div itemprop="articleBody" data-enrich="blog_{{index}}">{{body}}</div>
33
+ </article>
34
+ </section>
@@ -0,0 +1,27 @@
1
+ <section itemscope itemtype="https://schema.org/Blog" itemid="{{{blogIri}}}">
2
+ <span itemprop="name">Engineering Blog</span>
3
+ {{#posts}}
4
+ <div
5
+ itemprop="blogPost"
6
+ itemscope
7
+ itemtype="https://schema.org/BlogPosting"
8
+ itemid="{{{iri}}}"
9
+ >
10
+ <h3 itemprop="headline">{{headline}}</h3>
11
+ <meta itemprop="identifier" content="{{identifier}}" />
12
+ <time itemprop="datePublished" datetime="{{date}}">{{date}}</time>
13
+ {{#author}}
14
+ <span
15
+ itemprop="author"
16
+ itemscope
17
+ itemtype="https://schema.org/Person"
18
+ itemid="{{{iri}}}"
19
+ >
20
+ <span itemprop="name">{{name}}</span>
21
+ </span>
22
+ {{/author}}
23
+ <link itemprop="isPartOf" href="{{{blogIri}}}" />
24
+ <link itemprop="url" href="blog-{{index}}.html" />
25
+ </div>
26
+ {{/posts}}
27
+ </section>
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: briefing
3
+ person: {{personId}}
4
+ date: {{date}}
5
+ ---
6
+
7
+ # Daily Briefing — {{personName}}
8
+
9
+ **Role:** {{discipline}} {{level}} **Team:** {{teamName}} ({{deptName}})
10
+ **Email:** {{email}}
11
+
12
+ ## Today's Focus
13
+
14
+ {{briefing}}
15
+
16
+ ## Quick Stats
17
+
18
+ - Team size: {{teamSize}}
19
+ - Level: {{level}}
20
+ - Manager: {{isManager}}
@@ -0,0 +1,15 @@
1
+ {{#comments}}
2
+ <div itemscope itemtype="https://schema.org/Comment" itemid="{{{iri}}}">
3
+ <span
4
+ itemprop="author"
5
+ itemscope
6
+ itemtype="https://schema.org/Person"
7
+ itemid="{{{authorIri}}}"
8
+ >
9
+ <span itemprop="name">{{author}}</span>
10
+ </span>
11
+ <span itemprop="text">{{body}}</span>
12
+ <meta itemprop="dateCreated" content="{{date}}" />
13
+ <link itemprop="about" href="{{{aboutIri}}}" />
14
+ </div>
15
+ {{/comments}}
@@ -0,0 +1,37 @@
1
+ {{#courses}}
2
+ <div itemscope itemtype="https://schema.org/Course" itemid="{{{iri}}}">
3
+ <h2 itemprop="name">{{title}}</h2>
4
+ <meta itemprop="identifier" content="{{id}}" />
5
+ <time itemprop="startDate">{{date}}</time>
6
+
7
+ <div
8
+ itemprop="provider"
9
+ itemscope
10
+ itemtype="https://schema.org/Organization"
11
+ itemid="{{{orgIri}}}"
12
+ >
13
+ <span itemprop="name">{{orgName}}</span>
14
+ </div>
15
+
16
+ {{#prerequisiteIris}}
17
+ <link itemprop="coursePrerequisites" href="{{{.}}}" />
18
+ {{/prerequisiteIris}} {{#platformLink}}
19
+ <link itemprop="isRelatedTo" href="{{{iri}}}" />
20
+ {{/platformLink}} {{#drugLink}}
21
+ <link itemprop="isRelatedTo" href="{{{iri}}}" />
22
+ {{/drugLink}} {{#attendees}}
23
+ <div
24
+ itemprop="attendee"
25
+ itemscope
26
+ itemtype="https://schema.org/Person"
27
+ itemid="{{{iri}}}"
28
+ >
29
+ <span itemprop="name">{{name}}</span>
30
+ </div>
31
+ {{/attendees}}
32
+
33
+ <div itemprop="description" data-enrich="course_{{id}}">
34
+ A course covering {{title}} for pharmaceutical professionals.
35
+ </div>
36
+ </div>
37
+ {{/courses}}
@@ -0,0 +1,29 @@
1
+ {{#departments}}
2
+ <div itemscope itemtype="https://schema.org/Organization" itemid="{{{iri}}}">
3
+ <span itemprop="name">{{name}}</span>
4
+ <div itemprop="department">
5
+ {{#teams}}
6
+ <div
7
+ itemscope
8
+ itemtype="https://schema.org/Organization"
9
+ itemid="{{{iri}}}"
10
+ >
11
+ <span itemprop="name">{{name}}</span>
12
+ <meta itemprop="numberOfEmployees" content="{{size}}" />
13
+ {{#members}}
14
+ <div
15
+ itemprop="member"
16
+ itemscope
17
+ itemtype="https://schema.org/Person"
18
+ itemid="{{{iri}}}"
19
+ >
20
+ <span itemprop="name">{{name}}</span>
21
+ <span itemprop="jobTitle">{{jobTitle}}</span>
22
+ <link itemprop="worksFor" href="{{{teamIri}}}" />
23
+ </div>
24
+ {{/members}}
25
+ </div>
26
+ {{/teams}}
27
+ </div>
28
+ </div>
29
+ {{/departments}}
@@ -0,0 +1,23 @@
1
+ {{#drugs}}
2
+ <div itemscope itemtype="https://schema.org/Drug" itemid="{{{iri}}}">
3
+ <h2 itemprop="name">{{name}}</h2>
4
+ <meta itemprop="identifier" content="{{id}}" />
5
+ <span itemprop="drugClass">{{drugClass}}</span>
6
+ <span itemprop="activeIngredient">{{activeIngredient}}</span>
7
+ <meta itemprop="legalStatus" content="{{legalStatus}}" />
8
+
9
+ {{#parentDrugIri}}
10
+ <link itemprop="isRelatedTo" href="{{{parentDrugIri}}}" />
11
+ {{/parentDrugIri}} {{#projectLinks}}
12
+ <link itemprop="isRelatedTo" href="{{{iri}}}" />
13
+ {{/projectLinks}} {{#platformLinks}}
14
+ <link itemprop="isRelatedTo" href="{{{iri}}}" />
15
+ {{/platformLinks}} {{#eventLinks}}
16
+ <link itemprop="isRelatedTo" href="{{{iri}}}" />
17
+ {{/eventLinks}}
18
+
19
+ <div itemprop="clinicalPharmacology" data-enrich="drug_{{id}}">
20
+ {{clinicalPharmacology}}
21
+ </div>
22
+ </div>
23
+ {{/drugs}}
@@ -0,0 +1,38 @@
1
+ {{#events}}
2
+ <div itemscope itemtype="https://schema.org/Event" itemid="{{{iri}}}">
3
+ <h2 itemprop="name">{{title}}</h2>
4
+ <time itemprop="startDate">{{date}}</time>
5
+ <span itemprop="location">{{location}}</span>
6
+ <meta itemprop="eventStatus" content="https://schema.org/{{eventStatus}}" />
7
+
8
+ {{#organizer}}
9
+ <div
10
+ itemprop="organizer"
11
+ itemscope
12
+ itemtype="https://schema.org/Person"
13
+ itemid="{{{iri}}}"
14
+ >
15
+ <span itemprop="name">{{name}}</span>
16
+ </div>
17
+ {{/organizer}} {{#attendees}}
18
+ <div
19
+ itemprop="attendee"
20
+ itemscope
21
+ itemtype="https://schema.org/Person"
22
+ itemid="{{{iri}}}"
23
+ >
24
+ <span itemprop="name">{{name}}</span>
25
+ </div>
26
+ {{/attendees}} {{#aboutProjects}}
27
+ <link itemprop="about" href="{{{iri}}}" />
28
+ {{/aboutProjects}} {{#aboutDrugs}}
29
+ <link itemprop="about" href="{{{iri}}}" />
30
+ {{/aboutDrugs}} {{#aboutPlatforms}}
31
+ <link itemprop="about" href="{{{iri}}}" />
32
+ {{/aboutPlatforms}}
33
+
34
+ <div itemprop="description" data-enrich="event_{{index}}">
35
+ {{title}} event at {{location}}.
36
+ </div>
37
+ </div>
38
+ {{/events}}
@@ -0,0 +1,22 @@
1
+ <section itemscope itemtype="https://schema.org/FAQPage">
2
+ {{#faqs}}
3
+ <div
4
+ itemscope
5
+ itemtype="https://schema.org/Question"
6
+ itemid="{{{iri}}}"
7
+ itemprop="mainEntity"
8
+ >
9
+ <span itemprop="name">{{question}}</span>
10
+ <div
11
+ itemscope
12
+ itemtype="https://schema.org/Answer"
13
+ itemprop="acceptedAnswer"
14
+ >
15
+ <span itemprop="text">{{answer}}</span>
16
+ </div>
17
+ {{#aboutLinks}}
18
+ <link itemprop="about" href="{{{iri}}}" />
19
+ {{/aboutLinks}}
20
+ </div>
21
+ {{/faqs}}
22
+ </section>
@@ -0,0 +1,8 @@
1
+ <div
2
+ itemscope
3
+ itemtype="https://schema.org/HowTo"
4
+ itemid="{{{domain}}}/howto/{{topic}}"
5
+ >
6
+ <h2 itemprop="name">{{title}}</h2>
7
+ <div itemprop="text"><p>{{prose}}</p></div>
8
+ </div>
@@ -0,0 +1,8 @@
1
+ {{#managers}}
2
+ <div itemscope itemtype="https://schema.org/Person" itemid="{{{iri}}}">
3
+ <span itemprop="name">{{name}}</span>
4
+ <span itemprop="jobTitle">Manager, {{teamName}}</span>
5
+ <span itemprop="email">{{email}}</span>
6
+ <link itemprop="worksFor" href="{{{departmentIri}}}" />
7
+ </div>
8
+ {{/managers}}
@@ -0,0 +1,34 @@
1
+ # Entity Ontology
2
+
3
+ Domain: `https://{{domain}}`
4
+
5
+ ## Organizations
6
+
7
+ {{#orgs}}
8
+
9
+ - [{{{name}}}]({{{iri}}}) {{/orgs}}
10
+
11
+ ## Departments
12
+
13
+ {{#departments}}
14
+
15
+ - [{{{name}}}]({{{iri}}}) {{/departments}}
16
+
17
+ ## Teams
18
+
19
+ {{#teams}}
20
+
21
+ - [{{{name}}}]({{{iri}}}) {{/teams}}
22
+
23
+ ## People
24
+
25
+ {{#people}}
26
+
27
+ - [{{{name}}}]({{{iri}}}) — {{discipline}} {{level}} {{/people}} {{#hasMore}}
28
+ - ... and {{moreCount}} more {{/hasMore}}
29
+
30
+ ## Projects
31
+
32
+ {{#projects}}
33
+
34
+ - [{{{name}}}]({{{iri}}}) {{/projects}}
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>{{title}}</title>
6
+ <base href="{{{domain}}}/" />
7
+ </head>
8
+ <body>
9
+ <h1>{{title}}</h1>
10
+ {{{body}}}
11
+ </body>
12
+ </html>