[READ-ONLY] Mirror of https://github.com/flo-bit/svleek. simple but beautiful documentation generator from markdown flo-bit.github.io/svleek/
documentation documentation-generator markdown svelte sveltekit tailwind
0

Configure Feed

Select the types of activity you want to include in your feed.

svleek / build-docs.js
15 kB 423 lines
1import fs from 'fs'; 2import path from 'path'; 3import { JSDOM } from 'jsdom'; 4import markdownit from 'markdown-it'; 5import hljs from 'highlight.js'; // https://highlightjs.org 6 7const docsPath = './docs'; 8const routesPath = './src/routes/docs'; 9const staticPath = './static'; 10 11let config; 12const menu = {}; 13 14function addClassesToElement(element, classes) { 15 classes.split(' ').forEach((className) => { 16 element.classList.add(className); 17 }); 18} 19 20function addAllTailwindClassesToElement(dom) { 21 dom.window.document.querySelectorAll('h1').forEach((element) => { 22 addClassesToElement( 23 element, 24 'mt-16 text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100 sm:text-4xl' 25 ); 26 }); 27 dom.window.document.querySelectorAll('p').forEach((element) => { 28 addClassesToElement(element, 'mt-6 leading-8'); 29 }); 30 dom.window.document.querySelectorAll('h2').forEach((element) => { 31 addClassesToElement( 32 element, 33 'mt-16 text-2xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100' 34 ); 35 }); 36 dom.window.document.querySelectorAll('h3').forEach((element) => { 37 addClassesToElement(element, 'mt-6 text-xl leading-8'); 38 }); 39 dom.window.document.querySelectorAll('img').forEach((element) => { 40 // add base_url to src, if it doesn't start with http 41 const src = element.getAttribute('src'); 42 if (!src.startsWith('http')) element.setAttribute('src', config.base_url + src); 43 44 addClassesToElement(element, 'rounded-xl bg-neutral-50 dark:bg-neutral-950 object-cover'); 45 }); 46 dom.window.document.querySelectorAll('blockquote').forEach((element) => { 47 addClassesToElement( 48 element, 49 'font-semibold text-neutral-900 dark:text-neutral-100 mt-10 border-l border-accent-600 dark:border-accent-500 pl-9' 50 ); 51 }); 52 dom.window.document.querySelectorAll('a').forEach((element) => { 53 const href = element.getAttribute('href'); 54 if (!href.startsWith('http')) element.setAttribute('href', config.base_url + href); 55 56 addClassesToElement( 57 element, 58 'font-semibold text-accent-600 hover:text-accent-500 dark:text-accent-500 dark:hover:text-accent-400' 59 ); 60 }); 61 62 dom.window.document.querySelectorAll('ul').forEach((element) => { 63 addClassesToElement(element, 'mt-8 space-y-4 text-neutral-600 dark:text-neutral-400'); 64 }); 65 dom.window.document.querySelectorAll('ol').forEach((element) => { 66 addClassesToElement(element, 'mt-8 space-y-4 text-neutral-600 dark:text-neutral-400'); 67 }); 68 dom.window.document.querySelectorAll('ul>li').forEach((element) => { 69 const content = element.innerHTML; 70 // find first : 71 const firstColonIndex = content.indexOf(':'); 72 if (firstColonIndex === -1) { 73 element.innerHTML = `<span class="text-neutral-900 dark:text-neutral-100 font-semibold">${content}</span>`; 74 } else { 75 const firstPart = content.substring(0, firstColonIndex); 76 const secondPart = content.substring(firstColonIndex + 1); 77 element.innerHTML = `<span><span class="text-neutral-900 dark:text-neutral-100 font-semibold">${firstPart}</span> ${secondPart}</span>`; 78 } 79 const circle = dom.window.document.createElement('div'); 80 circle.setAttribute( 81 'class', 82 'mt-3 h-1.5 w-1.5 flex-none bg-accent-600 dark:bg-accent-500 rounded-full' 83 ); 84 circle.setAttribute('aria-hidden', 'true'); 85 element.prepend(circle); 86 87 addClassesToElement(element, 'flex gap-x-3 items-start'); 88 }); 89 90 dom.window.document.querySelectorAll('ol>li').forEach((element) => { 91 const content = element.innerHTML; 92 // find first : 93 const firstColonIndex = content.indexOf(':'); 94 if (firstColonIndex === -1) { 95 element.innerHTML = `<span class="text-neutral-900 dark:text-neutral-100 font-semibold">${content}</span>`; 96 } else { 97 const firstPart = content.substring(0, firstColonIndex); 98 const secondPart = content.substring(firstColonIndex + 1); 99 element.innerHTML = `<span><span class="text-neutral-900 dark:text-neutral-100 font-semibold">${firstPart}</span> ${secondPart}</span>`; 100 } 101 const num = dom.window.document.createElement('div'); 102 num.setAttribute( 103 'class', 104 'flex-none text-accent-600 dark:text-accent-500 font-semibold text-lg' 105 ); 106 num.setAttribute('aria-hidden', 'true'); 107 // find out the number 108 const itemsArray = Array.from(element.closest('ol').querySelectorAll('li')); 109 110 // Find the index of the element 111 const number = itemsArray.indexOf(element) + 1; 112 num.innerHTML = number; 113 element.prepend(num); 114 115 addClassesToElement(element, 'flex gap-x-3 items-start'); 116 }); 117 118 dom.window.document.querySelectorAll('pre').forEach((element) => { 119 // Create a new div element 120 let wrapperDiv = dom.window.document.createElement('div'); 121 wrapperDiv.setAttribute('class', 'relative'); 122 123 // Insert the new div right before the pre element in the DOM 124 element.parentNode.insertBefore(wrapperDiv, element); 125 126 // Move the pre element inside the new div 127 wrapperDiv.appendChild(element); 128 addClassesToElement( 129 element, 130 'rounded-xl bg-neutral-100 dark:bg-neutral-900 p-4 mt-6 relative overflow-x-scroll' 131 ); 132 // add button to copy the code 133 const copyButton = dom.window.document.createElement('button'); 134 copyButton.setAttribute( 135 'class', 136 'absolute top-2 right-2 text-xs text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-300' 137 ); 138 139 // remove the leading and trailing newlines 140 copyButton.innerHTML = `Copy`; 141 element.setAttribute( 142 'onclick', 143 'navigator.clipboard.writeText(this.querySelector("code").textContent.trim())' 144 ); 145 wrapperDiv.appendChild(copyButton); 146 }); 147 148 // find blockquotes that start with [!WARNING] 149 dom.window.document.querySelectorAll('blockquote').forEach((element) => { 150 // get first p 151 const firstP = element.querySelector('p'); 152 if (firstP?.textContent?.startsWith('[!WARNING]')) { 153 // rename blockquote to warning 154 const warning = dom.window.document.createElement('div'); 155 addClassesToElement( 156 warning, 157 'rounded-md bg-yellow-50 dark:bg-yellow-500/5 p-4 dark:ring-1 dark:ring-yellow-500/10 mt-6' 158 ); 159 const flex = dom.window.document.createElement('div'); 160 warning.appendChild(flex); 161 addClassesToElement(flex, 'flex'); 162 const svg = dom.window.document.createElement('svg'); 163 flex.appendChild(svg); 164 addClassesToElement(svg, 'h-5 w-5 text-yellow-500 flex-shrink-0'); 165 svg.setAttribute('viewBox', '0 0 20 20'); 166 svg.setAttribute('fill', 'currentColor'); 167 svg.setAttribute('aria-hidden', 'true'); 168 const path = dom.window.document.createElement('path'); 169 svg.appendChild(path); 170 path.setAttribute('fill-rule', 'evenodd'); 171 path.setAttribute( 172 'd', 173 'M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z' 174 ); 175 const div = dom.window.document.createElement('div'); 176 flex.appendChild(div); 177 addClassesToElement(div, 'ml-3'); 178 const h3 = dom.window.document.createElement('h3'); 179 div.appendChild(h3); 180 addClassesToElement(h3, 'text-sm font-medium text-yellow-800 dark:text-yellow-400'); 181 h3.innerHTML = 'Attention needed'; 182 const div2 = dom.window.document.createElement('div'); 183 div.appendChild(div2); 184 addClassesToElement(div2, 'mt-2 text-sm text-yellow-700 dark:text-yellow-300'); 185 const p = dom.window.document.createElement('p'); 186 div2.appendChild(p); 187 p.innerHTML = firstP.textContent.replace('[!WARNING]', ''); 188 189 element.parentNode.insertBefore(warning, element); 190 element.remove(); 191 } 192 }); 193 194 dom.window.document.querySelectorAll('hr').forEach((element) => { 195 addClassesToElement(element, 'mt-8 opacity-10'); 196 }); 197 198 let allCodeElements = Array.from(dom.window.document.querySelectorAll('code')); 199 let filteredCodeElements = allCodeElements.filter((el) => !el.closest('pre')); 200 201 filteredCodeElements.forEach((element) => { 202 addClassesToElement(element, 'rounded-md bg-neutral-200 dark:bg-neutral-800 p-1'); 203 }); 204} 205 206function convertMarkdownToHTML(markdown) { 207 const md = markdownit({ 208 highlight: function (str, lang) { 209 if (lang && hljs.getLanguage(lang)) { 210 try { 211 str = hljs.highlight(str, { language: lang }).value; 212 // remove the leading and trailing newlines 213 //str = str.replace(/^\n+|\n+$/g, ''); 214 return str; 215 } catch (__) { 216 console.log(__); 217 } 218 } 219 220 return ''; // use external default escaping 221 } 222 }); 223 const content = md.render(markdown); 224 const dom = new JSDOM(content); 225 addAllTailwindClassesToElement(dom); 226 227 const div1 = dom.window.document.createElement('div'); 228 div1.setAttribute('class', 'px-6 py-16 lg:px-8 max-w-[100vw]'); 229 const div2 = dom.window.document.createElement('div'); 230 div2.setAttribute( 231 'class', 232 'mx-auto max-w-3xl text-base leading-7 text-neutral-700 dark:text-neutral-300' 233 ); 234 div1.appendChild(div2); 235 div2.innerHTML = dom.window.document.querySelector('body').innerHTML; 236 dom.window.document.querySelector('body').innerHTML = div1.outerHTML; 237 238 // replace { and } with &#123; and &#125; 239 return dom.window.document 240 .querySelector('body') 241 .innerHTML.replace(/{/g, '&#123;') 242 .replace(/}/g, '&#125;'); 243} 244 245function processFile(filePath, destPath) { 246 const markdown = fs.readFileSync(filePath, 'utf-8'); 247 const html = convertMarkdownToHTML(markdown); 248 const pageContent = `${html}`; 249 // create the directory if it doesn't exist 250 if (!fs.existsSync(destPath)) { 251 fs.mkdirSync(destPath, { recursive: true }); 252 } 253 fs.writeFileSync(`${destPath}/+page.svelte`, pageContent); 254} 255 256function processDirectory(dirPath, destPath, parentMenu = null) { 257 if (!fs.existsSync(destPath)) { 258 fs.mkdirSync(destPath, { recursive: true }); 259 } 260 261 const files = fs.readdirSync(dirPath).sort(); 262 files.forEach((file) => { 263 const filePath = path.join(dirPath, file); 264 const fileStats = fs.statSync(filePath); 265 266 if (!fileStats.isDirectory() && !file.endsWith('.md')) { 267 return; 268 } 269 // Remove the leading number and hyphen and the trailing .md 270 const fileName = file.replace(/^\d+-/, '').replace(/\.md$/, ''); 271 const newDestPath = path.join(destPath, fileName); 272 273 // add to menu 274 const menuEntry = { 275 // replace - with space and turn the first letter to uppercase 276 title: fileName.replace(/-/g, ' ').replace(/^\w/, (c) => c.toUpperCase()), 277 // remove src/routes from the path 278 href: config.base_url + newDestPath.replace(/^src\/routes/, '') 279 }; 280 if (parentMenu) { 281 parentMenu.children = parentMenu.children || []; 282 parentMenu.children.push(menuEntry); 283 } 284 if (fileStats.isDirectory()) { 285 processDirectory(filePath, newDestPath, menuEntry); 286 } else { 287 processFile(filePath, newDestPath); 288 } 289 }); 290} 291 292function addMenu(menu) { 293 // find let menu = [xyz]; in +layout.svelte and replace it with the menu 294 const layoutPath = './src/routes/docs/+layout.svelte'; 295 const layoutContent = fs.readFileSync(layoutPath, 'utf-8'); 296 const newLayoutContent = layoutContent.replace( 297 /let menu = \[(\S|\s)*\];/, 298 `let menu = ${JSON.stringify(menu.children, null, 2)};` 299 ); 300 fs.writeFileSync(layoutPath, newLayoutContent); 301} 302 303function cleanFolder(folder) { 304 // delete everything except +layout.svelte from target folder 305 const files = fs.readdirSync(folder); 306 files.forEach((file) => { 307 const filePath = path.join(folder, file); 308 const fileStats = fs.statSync(filePath); 309 if (fileStats.isDirectory()) { 310 fs.rmdirSync(filePath, { recursive: true }); 311 } else if (file !== '+layout.svelte') { 312 fs.unlinkSync(filePath); 313 } 314 }); 315} 316 317function readConfig() { 318 // read from docs/config.json 319 const configPath = docsPath + '/svleek.config.json'; 320 const configContent = fs.readFileSync(configPath, 'utf-8'); 321 const config = JSON.parse(configContent); 322 return config; 323} 324 325function copyAssets(srcDir, destDir) { 326 // Create destination directory if it doesn't exist 327 if (!fs.existsSync(destDir)) { 328 fs.mkdirSync(destDir, { recursive: true }); 329 } 330 331 // Read source directory contents 332 const items = fs.readdirSync(srcDir); 333 334 items.forEach((item) => { 335 const srcPath = path.join(srcDir, item); 336 const destPath = path.join(destDir, item); 337 338 // Check if the item is a directory or file 339 const stat = fs.statSync(srcPath); 340 if (stat.isDirectory()) { 341 // Recursively copy directory 342 copyAssets(srcPath, destPath); 343 } else { 344 // Copy file 345 fs.copyFileSync(srcPath, destPath); 346 } 347 }); 348} 349 350function setupStartPage() { 351 const heroScreenshot = config.image; 352 const heroTitle = config.title; 353 const heroSubtitle = config.subtitle; 354 const heroButtonHref = config.first_page; 355 const heroButtonLabel = config.button_label; 356 357 const pagePath = './src/routes/+page.svelte'; 358 const pageContent = fs.readFileSync(pagePath, 'utf-8'); 359 const newPageContent = pageContent.replace( 360 /<HeroScreenshot [\s|\S]*\/>/, 361 `<HeroScreenshot title="${heroTitle}" subtitle="${heroSubtitle}" image="${ 362 config.base_url + heroScreenshot 363 }" buttonHref="${config.base_url + heroButtonHref}" buttonLabel="${heroButtonLabel}"/>` 364 ); 365 fs.writeFileSync(pagePath, newPageContent); 366} 367 368function setLogo() { 369 // read logo from ./docs/logo.svg 370 let logo = ''; 371 try { 372 const svgPath = docsPath + '/logo.svg'; 373 const svgContent = fs.readFileSync(svgPath, 'utf-8'); 374 logo = svgContent.replace(/<svg/, '<svg class="{size} fill-accent-600 dark:fill-accent-600"'); 375 } catch (e) { 376 console.warn('No logo.svg found in ./docs'); 377 } 378 379 // replace <slot></slot> in ./src/lib/Logo.svelte with the logo 380 const logoPath = './src/lib/Logo.svelte'; 381 const logoContent = fs.readFileSync(logoPath, 'utf-8'); 382 383 let newLogoContent = logoContent.replace(/<slot>[\s|\S]*<\/slot>/, `<slot>${logo}</slot>`); 384 385 // replace <a href="link" with <a href="config.base_url" 386 newLogoContent = newLogoContent.replace(/<a href="(.*)">/, `<a href="${config.base_url}">`); 387 388 console.log(newLogoContent == logoContent); 389 fs.writeFileSync(logoPath, newLogoContent); 390} 391 392function setGithubLink() { 393 // set link in ./src/lib/Github.svelte 394 // from config.github 395 const githubPath = './src/lib/Github.svelte'; 396 const githubContent = fs.readFileSync(githubPath, 'utf-8'); 397 const newGithubContent = githubContent.replace( 398 /let link = '.*';/, 399 `let link = '${config.github}';` 400 ); 401 fs.writeFileSync(githubPath, newGithubContent); 402} 403 404function setAccentColor() { 405 const tailwindConfigPath = './tailwind.config.js'; 406 const tailwindConfigContent = fs.readFileSync(tailwindConfigPath, 'utf-8'); 407 const newTailwindConfigContent = tailwindConfigContent.replace( 408 /colors: {\s*accent: colors\..*\s*}\s/, 409 `colors: { accent: colors.${config.accent} } ` 410 ); 411 fs.writeFileSync(tailwindConfigPath, newTailwindConfigContent); 412} 413 414config = readConfig(); 415setAccentColor(); 416setGithubLink(); 417setLogo(); 418cleanFolder(routesPath); 419cleanFolder(staticPath); 420copyAssets(docsPath, staticPath); 421processDirectory(docsPath, routesPath, menu); 422setupStartPage(); 423addMenu(menu);