blob: 51938a7f1268475cca1e407ec6b2f6a083ba58b1 [file] [log] [blame]
Dirk Prankedb03b662021-11-19 09:15:15 -08001// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// This file implements support for the "subpages" extension. If a
16// page author inserts `{% subpages collections.all %}` into a document,
17// this function will find all of the pages that are sub-pages of
18// the specified page (sub-pages in the sense that /blink/design-documents
19// is a sub-page of /blink) and display them in a hierarchical tree
20// format. `pageUrl` should be the path of the current page (Eleventy's
21// `page.url`) and `collectionOfAllPages` should be Eleventy's
22// `collections.all`.
23//
24// TODO(crbug.com/1271672): Figure out how to make this cleaner so the
25// syntax is less clunky.
26function render(pageUrl, collectionOfAllPages) {
27 let topPage = new Page('', pageUrl);
28
29 // the pages in `collectionOfAllPages` with `pageUrl` as an ancestor.
30 let subPages = [];
31 for (const item of collectionOfAllPages) {
32 if (item.data.page.url.startsWith(topPage.url)) {
33 subPages.push(new Page(item.data.title, item.data.page.url));
34 }
35 }
36 subPages.sort(byProperty('title'));
37
38 // A mapping from URLs to Pages for `pageUrl` and all its sub-pages.
39 const pageMap = new Map();
40 pageMap.set(topPage.url, topPage);
41
42 // Now build the mapping of sub-pages to pages.
43 for (subPage of subPages) {
44 pageMap.set(subPage.url, subPage);
45 if (pageMap.has(subPage.parentPage)) {
46 pageMap.get(subPage.parentPage).subPages.push(subPage);
47 }
48 }
49
50 let html = ('<nav class="subpage-listing">\n' +
51 ' <h4>Subpage Listing</h4>\n' +
52 ' <ul>\n');
53
54 for (const subPage of subPages) {
55 html += ' <li>\n' + subPage.walk(3);
56 }
57 html += (' </ul>\n' +
58 '</nav>\n');
59
60 return html;
61}
62
63class Page {
64 constructor(title, url) {
65 this.title = title;
66 this.url = rtrim(url, '/');
67 this.parentPage = dirname(this.url);
68
69 // This holds only the immediate sub-pages of the page, not the
70 // transitive closure of all sub-pages.
71 this.subPages = [];
72 }
73
74 // walk over the transitive closure of all of the page's subpages,
75 // and return an html fragment describing them as a tree of
76 // links and <details> elements (when a page has subpages).
77 // `indentDepth` is the number of levels to indent the HTML fragment.
78 walk(indentDepth) {
79 const indent = ' '.repeat(indentDepth);
80
81 this.subPages.sort(byProperty('title'));
82
83 if (this.subPages.length) {
84 let html = (`${indent}<details>\n` +
85 `${indent} <summary><a href="${this.url}">${
86 this.title}</a></summary>\n` +
87 `${indent} <ul>\n`);
88
89 for (const subPage of this.subPages) {
90 html += (`${indent} <li>\n` +
91 `${subPage.walk(indentDepth + 3)}`);
92 }
93 html += (
94 `${indent} </ul>\n` +
95 `${indent}</details>\n`
96 );
97 return html;
98 } else {
99 return `${indent}<a href="${this.url}">${this.title}</a>\n`;
100 }
101 }
102}
103
104// Returns the directory above the `path`, e.g.:
105// `dirname("/foo/bar")` returns "/foo".
106// Note that `dirname("/foo/bar/") also returns "/foo").
107function dirname(path) {
108 comps = path.split('/');
109 return comps.slice(0, comps.length - 1).join('/');
110}
111
112// Returns a copy of the string `s` with the rightmost `ch` removed.
113function rtrim(s, ch) {
114 if (s.endsWith(ch)) {
115 return s.substr(0, s.length - 1);
116 }
117 return s;
118}
119
120// Returns a comparison function that will compare two objects by
121// the lower-cased values of the specified property.
122function byProperty(prop) {
123 return (x, y) => {
124 a = x[prop].toLowerCase();
125 b = y[prop].toLowerCase();
126 return (a > b ? 1 : (a === b ? 0 : -1));
127 }
128}
129
130exports.render = render;