21 |
57 |
58 |
59 |
67 |
68 | {{#each pages}}
69 |
70 |
{{title}}
71 | {{{content}}}
72 |
73 |
74 | {{#each children}}
75 |
76 |
{{title}}
77 | {{{content}}}
78 |
79 | {{/each}}
80 | {{/each}}
81 |
82 |
89 |
90 |
91 |
94 | {{#each htmlBody}}
95 | {{{this}}}
96 | {{/each}}
97 |
98 |
99 |
--------------------------------------------------------------------------------
/templates/alchemy/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the current hash (without the leading #)
3 | * @returns {string}
4 | */
5 | const currentHash = () => window.location.hash.substr(1) || '';
6 |
7 | /**
8 | * Get all pages element
9 | * @returns {Element[]}
10 | */
11 | const getPageElements = () => [...document.querySelectorAll('.content .page')];
12 |
13 | const pageEls = getPageElements();
14 |
15 | /**
16 | * Get current page index from an hash string
17 | * @param {number} hash
18 | */
19 | const getCurrentPageIndex = hash => pageEls.findIndex((page, i) => page.id === hash || (i === 0 && hash === ''));
20 |
21 | /**
22 | * Display class from hash string
23 | * @param {string} hash
24 | */
25 | const showPageFromHash = (hash) => {
26 | let activeIndex = 0;
27 | pageEls.forEach((page, i) => {
28 | if (page.id === hash || (i === 0 && hash === '')) {
29 | activeIndex = i;
30 | page.classList.remove('hide');
31 | } else {
32 | page.classList.add('hide');
33 | }
34 | });
35 | return activeIndex;
36 | };
37 |
38 | /**
39 | * Toggle class for an element
40 | * @param {Element} element
41 | * @param {string} className
42 | */
43 | const toggleClass = (element, className) => {
44 | if (element.classList !== null && element.classList.contains(className)) {
45 | element.classList.remove(className);
46 | } else {
47 | element.classList.add(className);
48 | }
49 | };
50 |
51 | /**
52 | * Highlight the given word
53 | * @param {Element} root
54 | * @param {string} word
55 | * @param {string} [className='highlight'] className
56 | */
57 | function highlightWord(root, word, className = 'highlight') {
58 | const excludeElements = ['script', 'style', 'iframe', 'canvas'];
59 | let found = false;
60 |
61 | /**
62 | * @returns {Node[]}
63 | */
64 | const textNodesUnder = () => {
65 | const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
66 | const text = [];
67 |
68 | while (walk.nextNode()) {
69 | if (excludeElements.indexOf(walk.currentNode.parentElement.tagName.toLowerCase()) < 0) {
70 | text.push(walk.currentNode);
71 | }
72 | }
73 | return text;
74 | };
75 |
76 | /**
77 | * Highlight words
78 | * @param {Node} n
79 | * @returns {boolean}
80 | */
81 | const highlightWords = (n) => {
82 | const indexOfNode = (node, i) => node.nodeValue.toLowerCase().indexOf(word.toLowerCase(), i);
83 | let after;
84 | let span;
85 | let i = indexOfNode(n);
86 |
87 | if (!found && i > -1) {
88 | found = true;
89 | }
90 |
91 | while (i > -1) {
92 | after = n.splitText(i + word.length);
93 | span = document.createElement('span');
94 | span.className = className;
95 | span.appendChild(n.splitText(i));
96 | after.parentNode.insertBefore(span, after);
97 | n = after;
98 | i = indexOfNode(after, i);
99 | }
100 | };
101 |
102 | textNodesUnder().forEach(highlightWords);
103 | return found;
104 | }
105 |
106 | /**
107 | * Remove highlight from elements
108 | * @param {Element[]} elements
109 | */
110 | const removeHighlight = (elements) => {
111 | elements.forEach((el) => {
112 | const prev = el.previousSibling;
113 | const next = el.nextSibling;
114 | prev.textContent += (el.textContent + next.textContent);
115 | el.parentNode.removeChild(next);
116 | el.parentNode.removeChild(el);
117 | });
118 | };
119 |
120 | const sidebarLinks = document.querySelectorAll('.sidebar ul li a');
121 |
122 | /**
123 | * Highlight sidebar link
124 | * @param {number} index
125 | */
126 | const addClassSidebarIndex = (index, className) => {
127 | sidebarLinks[index].classList.add(className);
128 | };
129 |
130 | /**
131 | * Remove highlights from sidebar
132 | * @param {string} className
133 | */
134 | const removeClassesSidebar = (className) => {
135 | sidebarLinks.forEach(link => link.classList.remove(className));
136 | };
137 |
138 | const toggleActiveLinkSidebar = (index) => {
139 | sidebarLinks.forEach((link, i) => {
140 | if (i === index) {
141 | link.classList.add('active');
142 | } else {
143 | link.classList.remove('active');
144 | }
145 | });
146 | };
147 |
148 | const main = document.querySelector('.main');
149 | document.querySelector('button.toggle-sidebar').onclick = () => {
150 | toggleClass(main, 'full-width');
151 | };
152 |
153 | document.querySelector('button.toggle-light').onclick = () => {
154 | toggleClass(main, 'dark');
155 | };
156 |
157 | document.querySelector('button.next-page').onclick = () => {
158 | const nextIndex = Math.min(getCurrentPageIndex(currentHash()) + 1, pageEls.length - 1);
159 | window.location.hash = `#${pageEls[nextIndex].id}`;
160 | };
161 |
162 | document.querySelector('button.previous-page').onclick = () => {
163 | const prevIndex = Math.max(0, (getCurrentPageIndex(currentHash()) - 1));
164 | window.location.hash = `#${pageEls[prevIndex].id}`;
165 | };
166 |
167 | document.querySelector('.tools').classList.remove('hide');
168 |
169 | const searchInputEl = document.querySelector('.search-input');
170 | searchInputEl.classList.remove('hide');
171 |
172 | const highlightClass = 'highlight';
173 |
174 | searchInputEl.addEventListener('keypress', (e) => {
175 | const key = e.which || e.keyCode;
176 | if (key === 13) {
177 | removeHighlight([...document.querySelectorAll('.content .highlight')]);
178 | removeClassesSidebar(highlightClass);
179 | pageEls.forEach((el, pIndex) => {
180 | if (highlightWord(el, searchInputEl.value)) {
181 | addClassSidebarIndex(pIndex, highlightClass);
182 | }
183 | });
184 | }
185 | });
186 |
187 | searchInputEl.addEventListener('keyup', (e) => {
188 | const key = e.which || e.keyCode;
189 | if (key !== 13 && searchInputEl.value.length < 1) {
190 | removeHighlight([...document.querySelectorAll(`.content .${highlightClass}`)]);
191 | removeClassesSidebar(highlightClass);
192 | }
193 | });
194 |
195 | const showPageFromCurrentHash = () => {
196 | const index = showPageFromHash(currentHash());
197 | toggleActiveLinkSidebar(index);
198 | };
199 |
200 | if (document.body.clientWidth < 768) {
201 | toggleClass(main, 'full-width');
202 | }
203 |
204 | // Listen for hash change
205 | window.onhashchange = () => {
206 | showPageFromCurrentHash();
207 | };
208 |
209 | showPageFromCurrentHash();
210 |
--------------------------------------------------------------------------------
/templates/alchemy/style.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Lato:300,700|Merriweather:300italic,300|Inconsolata:400,700);
2 |
3 | :root {
4 | --mainColor: #373f52;
5 | --secondaryColor: #d5dae6;
6 | --mainBackgroundColor: #fff;
7 | --sidebarMainColor: #d5dae6;
8 | --sidebarBackgroundColor: #373f52;
9 | --sidebarSearchBackgroundColor: #40495f;
10 | }
11 | .dark {
12 | --mainColor: #d5dae6;
13 | --secondaryColor: #373f52;
14 | --mainBackgroundColor: #2b3140;
15 | --sidebarMainColor: #d5dae6;
16 | --sidebarBackgroundColor: #373f52;
17 | --sidebarSearchBackgroundColor: #40495f;
18 | }
19 | *, :after, :before {
20 | box-sizing: inherit;
21 | }
22 | html, body {
23 | box-sizing: border-box;
24 | height: 100%;
25 | width: 100%;
26 | }
27 | body {
28 | margin: 0;
29 | font-size: 16px;
30 | line-height: 1.6875em;
31 | font-family: Lato, sans-serif;
32 | }
33 | a {
34 | transition: all .2s linear;
35 | }
36 | hr {
37 | border: 0;
38 | margin: 30px 0;
39 | border-bottom: 1px solid #ddd;
40 | }
41 | .main {
42 | display: -ms-flexbox;
43 | display: -ms-flex;
44 | display: flex;
45 | -ms-flex-pack: end;
46 | justify-content: flex-end;
47 | min-height: 100%;
48 | background-color: var(--mainBackgroundColor);
49 | }
50 | .sidebar {
51 | display: flex;
52 | -webkit-box-orient: vertical;
53 | -moz-box-orient: vertical;
54 | -webkit-box-direction: normal;
55 | -moz-box-direction: normal;
56 | min-height: 0;
57 | flex-direction: column;
58 | width: 270px;
59 | height: 100%;
60 | position: fixed;
61 | top: 0;
62 | left: 0;
63 | z-index: 4;
64 | font-size: 15px;
65 | line-height: 18px;
66 | background: var(--sidebarBackgroundColor);
67 | color: var(--sidebarMainColor);
68 | overflow: hidden;
69 | transition: transform ease-in-out .25s;
70 | }
71 | .sidebar ul.menu {
72 | margin-bottom: 44px;
73 | overflow-y: auto;
74 | }
75 | .sidebar ul {
76 | list-style: none;
77 | margin: 0;
78 | padding: 0;
79 | }
80 | .sidebar ul > li > ul a {
81 | padding-left: 34px;
82 | height: 38px;
83 | line-height: 38px;
84 | }
85 | .sidebar ul li {
86 | margin: 0;
87 | padding: 0;
88 | line-height: 0;
89 | }
90 | .sidebar ul a {
91 | display: block;
92 | text-overflow: ellipsis;
93 | white-space: nowrap;
94 | overflow: hidden;
95 | width: 100%;
96 | color: var(--sidebarMainColor);
97 | padding: 0 14px;
98 | height: 40px;
99 | line-height: 40px;
100 | text-decoration: none;
101 | margin: 0;
102 | border-left: 2px solid transparent;
103 | }
104 | .sidebar ul a.active {
105 | background: rgba(255, 255, 255, .04);
106 | border-left: 2px solid rgba(255, 255, 255, .6);
107 | }
108 | .sidebar a:hover, .sidebar .header a:hover {
109 | color: #fff;
110 | }
111 | .sidebar ul a:hover {
112 | background: rgba(255, 255, 255, .08);
113 | }
114 | .sidebar .header a {
115 | color: var(--sidebarSecondaryColor);
116 | text-decoration: none;
117 | }
118 | .sidebar .header {
119 | padding: 0 15px;
120 | padding-bottom: 15px;
121 | border-bottom: 1px solid rgba(255, 255, 255, .5);
122 | }
123 |
124 | .sidebar .logo {
125 | margin-top: 15px;
126 | float: right;
127 | max-width: 120px;
128 | display: block;
129 | max-height: 50px;
130 | }
131 |
132 | .sidebar .search-input {
133 | font-family: Lato, sans-serif;
134 | color: var(--sidebarMainColor);
135 | background-color: var(--sidebarSearchBackgroundColor);
136 | border: 0;
137 | padding: 14px;
138 | font-size: 15px;
139 | font-weight: normal;
140 | position: absolute;
141 | bottom: 0;
142 | width: 100%;
143 | transition: box-shadow .15s ease;
144 | outline: none;
145 | }
146 |
147 | .sidebar .search-input:hover {
148 | filter: brightness(.75);
149 | }
150 |
151 | .sidebar .search-input:focus {
152 | box-shadow: 0 0 0 1px rgba(255, 255, 255, .5) inset;
153 | }
154 |
155 | .full-width .sidebar {
156 | transform: translateX(-270px);
157 | will-change: transform;
158 | }
159 |
160 | .full-width .content {
161 | margin-left: 0;
162 | }
163 |
164 | .content {
165 | font-family: Merriweather, 'Book Antiqua', Georgia, 'Century Schoolbook', serif;
166 | font-size: 1em;
167 | line-height: 1.6875em;
168 | width: 100%;
169 | margin-left: 270px;
170 | overflow-y: auto;
171 | -webkit-overflow-scrolling: touch;
172 | height: 100%;
173 | position: relative;
174 | z-index: 3;
175 | padding: 0 2rem;
176 | transition: margin ease-in-out .25s;
177 | will-change: margin-left;
178 | color: var(--mainColor);
179 | }
180 |
181 | .content h1,.content h2,.content h3,.content h4,.content h5,.content h6 {
182 | font-family: Lato, sans-serif;
183 | font-weight: 700;
184 | line-height: 1.5em;
185 | word-wrap: break-word;
186 | }
187 |
188 | .content h1 {font-size:2em; margin:1em 0 .5em}
189 | .content h1.heading {color: var(--mainColor); opacity: .8; margin: 1.25em 0 .5em}
190 | .content h1 small {font-weight:300}
191 | .content h1 a.view-source {font-size:1.2rem}
192 | .content h2 {font-size:1.6em;margin:1em 0 .5em;font-weight:700}
193 | .content h3 {font-size:1.3em; margin:1em 0 .5em; font-weight:700}
194 | .content .page a {color: var(--mainColor)}
195 | .content a *,.content a :after,.content a :before,.content a:after,.content a:before {text-shadow:none}
196 | .content a:visited {color: var(--mainColor)}
197 | .content ul li {line-height:1.5em}
198 | .content ul li>p {margin:0}
199 | .content blockquote {font-style:italic;margin:.5em 0;padding:.25em 1.5em;border-left:3px solid #e1e1e1;display:inline-block}
200 | .content blockquote :first-child {padding-top:0;margin-top:0}
201 | .content blockquote :last-child {padding-bottom:0;margin-bottom:0}
202 | .content a.no-underline,.content pre a {color:#9768d1;text-shadow:none;text-decoration:none;background-image:none}
203 | .content a.no-underline:active,.content a.no-underline:focus,.content a.no-underline:hover,.content a.no-underline:visited,.content pre a:active,.content pre a:focus,.content pre a:hover,.content pre a:visited {color:#9768d1;text-decoration:none}
204 | .content code {color: #373f52; font-weight:400;background-color:#f7f9fc;vertical-align:baseline;border-radius:2px;padding:.1em .2em;border:1px solid #d2ddee}
205 | .content pre {margin:1.5em 0; line-height: 1.5em}
206 | .content img {max-width: 100%}
207 |
208 | .content table {
209 | border-collapse: collapse;
210 | width: 100%;
211 | }
212 |
213 | .content th, .content td {
214 | padding: 0.3rem 0.5rem;
215 | text-align: left;
216 | border-bottom: 1px solid #e1e1e1;
217 | }
218 |
219 | .content .page {
220 | padding: 15px 0;
221 | transition: all .2s linear;
222 | /* min-height: 100%; */
223 | opacity: 1;
224 | }
225 |
226 | .content .page.hide {
227 | display: none;
228 | opacity: 0;
229 | }
230 |
231 | .footer {
232 | color: #999;
233 | font-size: .8em;
234 | font-style: italic;
235 | text-align: center;
236 | margin: 30px 0;
237 | }
238 |
239 | .footer a, .footer a:visited {
240 | color: #888;
241 | }
242 |
243 | .footer a:hover {
244 | color: #444;
245 | }
246 |
247 | .tools {
248 | opacity: .6;
249 | top: 4px;
250 | left: 6px;
251 | position: absolute;
252 | transition: opacity .15s linear;
253 | }
254 |
255 | .tools:hover {
256 | opacity: .9;
257 | }
258 |
259 | .tools.hide, .search-input.hide {
260 | display: none;
261 | }
262 |
263 | .tools button {
264 | color: var(--mainColor);
265 | float: left;
266 | font-size: 16px;
267 | height: 30px;
268 | width: 30px;
269 | margin: 0;
270 | background: transparent;
271 | margin-right: 4px;
272 | font-weight: 300;
273 | border: none;
274 | line-height: 0;
275 | transition: background .2s ease;
276 | outline: none;
277 | }
278 |
279 | .tools button:hover {
280 | background: rgba(0, 0, 0, .05);
281 | cursor: pointer;
282 | }
283 |
284 | .raindrop.icon {
285 | color: var(--mainColor);
286 | position: absolute;
287 | margin-left: 3px;
288 | margin-top: -3px;
289 | width: 10px;
290 | height: 10px;
291 | border: solid 1px currentColor;
292 | border-radius: 6px 6px 6px 0;
293 | -webkit-transform: rotate(135deg);
294 | transform: rotate(135deg);
295 | }
296 |
297 | .content .highlight {
298 | background: rgb(255, 252, 53, .5);
299 | }
300 |
301 | .sidebar a.highlight:after {
302 | position: absolute;
303 | content: " ";
304 | height: 10px;
305 | width: 10px;
306 | background: rgb(255, 252, 53, .8);
307 | border-radius: 9px;
308 | right: 6%;
309 | margin-top: 14px;
310 | }
311 |
312 | @media screen and (max-width: 768px) {
313 | .content {
314 | margin-left: 0;
315 | }
316 | .tools {
317 | margin-left: 270px;
318 | }
319 | .full-width .content .tools {
320 | margin-left: 0;
321 | }
322 | .full-width .sidebar {
323 | transition: none;
324 | }
325 | }
326 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | const test = require('ava');
2 | const {
3 | removeLeadingNumber,
4 | humanizesSlug,
5 | strToSlug,
6 | getBasename,
7 | getExtension,
8 | } = require('../src/helpers');
9 |
10 | test('test removeLeadingNumber function', (t) => {
11 | // it should remove the leading number
12 | t.is(removeLeadingNumber('0_ThisIsATest'), 'ThisIsATest');
13 | t.is(removeLeadingNumber('000_ThisIsATest'), 'ThisIsATest');
14 | t.is(removeLeadingNumber('12345_FAQ'), 'FAQ');
15 | t.is(removeLeadingNumber('09870_FAQ'), 'FAQ');
16 | t.is(removeLeadingNumber('-1_FAQ'), '-1_FAQ');
17 | t.is(removeLeadingNumber('_FAQ'), '_FAQ');
18 | t.is(removeLeadingNumber('ThisIsATest'), 'ThisIsATest');
19 | });
20 |
21 | test('test humanizesSlug function', (t) => {
22 | // it should humanize slug
23 | // t.is(humanizesSlug('0_ThisIsATest'), 'This Is A Test');
24 | t.is(humanizesSlug('12345_FAQ'), 'FAQ');
25 | t.is(humanizesSlug('-1_FAQ'), '-1 FAQ');
26 | t.is(humanizesSlug('This_is_a_test'), 'This is a test');
27 | t.is(humanizesSlug('1_FAQ'), 'FAQ');
28 | t.is(humanizesSlug('_FAQ'), 'FAQ');
29 | t.is(humanizesSlug('FAQ'), 'FAQ');
30 | // t.is(humanizesSlug('0_ThisIsATest'), 'This Is A Test');
31 | });
32 |
33 | test('test strToSlug function', (t) => {
34 | // it should transform a string to slug slug
35 | // t.is(strToSlug('This is a test'), 'Thissatest');
36 | t.is(strToSlug('A Test'), 'ATest');
37 | t.is(strToSlug('A Test'), 'ATest');
38 | // t.is(strToSlug('A_super Test'), 'AsuperTest');
39 | });
40 |
41 | test('test getBasename function', (t) => {
42 | // it should get the getBasename
43 | t.is(getBasename('document.txt'), 'document');
44 | t.is(getBasename('document.test.md'), 'document.test');
45 | });
46 |
47 | test('test getExtension function', (t) => {
48 | // it should get the extension
49 | t.is(getExtension('document.txt'), 'txt');
50 | t.is(getExtension('document.test.txt'), 'txt');
51 | t.is(getExtension('.txt'), '');
52 | t.is(getExtension('qwe'), '');
53 | t.is(getExtension(''), '');
54 | });
55 |
--------------------------------------------------------------------------------