diff --git a/lib.js b/lib.js index 2e27f5e..af5ef76 100644 --- a/lib.js +++ b/lib.js @@ -1,6 +1,7 @@ import Path from "path" import FS from "fs/promises" import { JSDOM } from "jsdom" +import Mustache from "mustache" @@ -286,11 +287,8 @@ export const createCache = async (cache = {}) => { return cache } -export const getCacheFilename = (source) => - source.name + '.xml' - export const getCachePath = (source, cache) => - Path.join(cache.path, getCacheFilename(source)) + Path.join(cache.path, source.cacheFilename) export const cacheSource = (source, cache) => write(getCachePath(source, cache), renderCache(source, cache)) @@ -346,7 +344,7 @@ export const renderCache = (source, cache) => `\ ${source.displayName} ${source.description} ${buildCacheLink(source)} - + ${new Date(source.latestPostDate).toUTCString()} rssssing ${source.items.map(item => item.outerHTML.replaceAll(/\n\s*/g, '')).join('\n')} @@ -467,6 +465,16 @@ export const createView = async (view = {}) => { if(view.imageStoreDirectory) await openImageStore(view) + if(isUnset(view.templatesPath)) { + view.templatesPath = Path.join(import.meta.dirname, 'templates') + } + + if(isUnset(view.stylesheetPath)) { + view.stylesheetPath = Path.join(import.meta.dirname, 'assets/style.css') + } + + view.batch.add(openTemplates(view)) + return view } @@ -481,32 +489,76 @@ export const openImageStore = async view => { let dirents = await FS.readdir(imageStorePath, { withFileTypes: true }) for(let dirent of dirents) { - if(dirent.isFile()) { - let basename = dirent.name.slice(0, dirent.name.lastIndexOf('.')) - view.imageStore.set(basename, Path.join(view.imageStoreDirectory, dirent.name)) - } + if(!dirent.isFile()) + continue + + let basename = dirent.name.slice(0, dirent.name.lastIndexOf('.')) + view.imageStore.set(basename, Path.join(view.imageStoreDirectory, dirent.name)) + } + + return view +} + +export const openTemplates = async (view) => { + view.templates = {} + let exists = await doesExist(view.templatesPath) + let dirents + + if(exists) + dirents = await FS.readdir(view.templatesPath, { withFileTypes: true }) + + if(!exists || dirents.length === 0) + throw new Error('Assets directory must contain a "nav" and "main" file.') + + for(let dirent of dirents) { + if(!dirent.isFile()) + continue + + let extensionStart = dirent.name.lastIndexOf('.') + let basename = dirent.name.slice(0, extensionStart) + let extension = dirent.name.slice(extensionStart + 1) + + if(basename == 'main' && isUnset(view.formatExtension)) + view.formatExtension = extension + + view.batch.add( + FS.readFile(Path.join(view.templatesPath, dirent.name), { encoding: 'utf-8' }) + // Must remove trailing newlines so partials will work cleanly + .then(template => view.templates[basename] = template ) + ) } return view } export const writeView = (sources, feeds, view) => { - view.header = renderNav(feeds, sources) + for(let source of sources) + labelList(source, view) + + for(let feed of feeds) + labelList(feed, view) + + view.header = renderNav(sources, feeds, view) let pages = [] + + for(let source of sources) { + pages = pages.concat(createPages(source, view)) + } for(let feed of feeds) { pages = pages.concat(createPages(feed, view)) } - for(let source of sources) { - pages = pages.concat(createPages(source, view)) - } - for(let page of pages) { writePage(page, view) } - writeStylesheet(Path.join(import.meta.dirname, 'assets/style.css'), view) + writeStylesheet(view.stylesheetPath, view) +} + +export const labelList = (list, view) => { + list.indexFilename = nameFinalPage(list, view) + list.empty = list.posts.length == 0 } export const createPages = (list, view) => { @@ -520,11 +572,12 @@ export const createPages = (list, view) => { posts.push(list.posts[i]) if(i % view.pageSize == 0) { - let title = getPageTitle(list, pages.length) - let filename = i < view.pageSize ? getFinalPageFilename(list) : getPageFilename(list, pages.length) + let filename = i < view.pageSize ? list.indexFilename : namePage(list, pages.length, view) + let page = { filename, - title, + title: list.displayName, + index: pages.length, posts: posts.reverse(), lastPageLink } @@ -546,152 +599,74 @@ export const createPages = (list, view) => { return pages } +export const namePage = (list, number, view) => + list.name + '-' + number + '.' + view.formatExtension + +export const nameFinalPage = (list, view) => + (list.main ? 'index' : list.name) + '.' + view.formatExtension + export const writePage = (page, view) => { - let html = renderPage(page.title, page.posts, view.header, renderNextPageLink(page.lastPageLink)) - let promise = write(Path.join(view.path, page.filename), html) + let content = renderPage(page, view) + let promise = write(Path.join(view.path, page.filename), content) - view.batch.add(promise.then(annotate(`Created "${page.title}" (${page.filename})`))) + view.batch.add(promise.then(annotate(`Created ${page.filename}`))) } -export const getFinalPageFilename = list => - (list.main ? 'index' : list.name) + '.html' +export const renderPage = (page, view) => + Mustache.render(view.templates.main, { + ...page, + sections: page.posts.map(createSection), + header: view.header, + footer: view.footer + }, view.templates) -export const getPageFilename = (list, i) => - list.name + '-' + i + '.html' +export const createMustacheDelimitedArray = (array) => { + if(isUnset(array) || array.length === 0) + return false -export const getPageTitle = (list, i) => - list.displayName + ' - ' + (i + 1) - -export const renderPage = (title, posts, header, footer) => `\ - - - -${title} - - - - - - -
-${header} -
-
- -${posts.map(renderPost).join('\n')} - -
-
-${footer} -
- - -` - -export const renderPost = (post, index) => { - let details = [] - - if(post.title) - details.push([ 'title', `"${post.title}"` ]) - - if(post.categories && post.categories.length > 0) - details.push([ 'categories', post.categories.map(name => `${name}`).join(', ') ]) - - details.push([ 'source', `${post.source.hostname}` ]) - details.push([ 'lists', post.occurances.map(occ => `${occ.list.displayName}`).join(', ') ]) - - return `\ -
-${post.images.map(renderImage).join('\n')} -
- -${post.source.displayName} (${post.source.type}) ${renderDate(new Date(post.date))} open -
    -${details.map(args => renderPostDetail(...args)).join('\n')} -
      - -
-
-
` + return { + final: array[array.length -1], + entries: array.slice(0, -1) + } } -export const renderPostDetail = (name, value) => - `
  • ${name} ${value}
  • ` +export const createSection = (post, index) => { + let date = new Date(post.date) -export const renderImage = href => { - return `\ -
    ` + return { + post, + index, + date: { + month: date.getMonth() + 1, + day: date.getDate(), + year: date.getFullYear() + }, + categories: createMustacheDelimitedArray(post.categories), + occurances: createMustacheDelimitedArray(post.occurances) + } } -export const renderDate = date => - (date.getMonth() + 1) + '.' + date.getDate() + '.' + date.getFullYear() - -export const renderNextPageLink = link => `\ -next` - -export const renderNav = (feeds, sources) => { - let sections = {} +export const renderNav = (sources, feeds, view) => { + let sourceTypes = [] for(let source of sources) { - let section = sections[source.type] + let section = sourceTypes[source.type] if(section) { section.push(source) } else { - sections[source.type] = [ + sourceTypes[source.type] = [ source ] } } - let out = '' - - for(let name in sections) { - out += ` -
  • - -${name}
    -${sections[name].map(renderNavEntry).join('\n')} - -
  • ` - } - - return `\ -
    - -Feeds -
    -
      - -${feeds.map(renderNavEntry).join('\n')} - -
    -
    -
      - -${out} - -
    -
    - -
    -
    ` + return Mustache.render(view.templates.nav, { + sourceTypes: Object.values(sourceTypes).map(createMustacheDelimitedArray), + feeds: createMustacheDelimitedArray(feeds), + }, view.templates) } -export const renderNavEntry = (list) => { - let extra = '' - - if(list.errored) { - extra += ' (errored)' - } else if (list.posts.length == 0) { - extra += ' (empty)' - } - - return `${list.displayName}${extra}` -} - - - // // ,-. ,-. . . ;-. ,-. ,-. // `-. | | | | | | |-' @@ -723,6 +698,8 @@ export const populateSource = async (channel, source, postReducerCallback, cache } export const createSource = async (source, getChannel, postReducerCallback, cache) => { + source.cacheFilename = source.name + '.xml' + if(cache.enabled) source = await openCache(source, cache) source = await populateSource(await getChannel(source), source, postReducerCallback, cache) diff --git a/package.json b/package.json index 49f0e01..53155ff 100755 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ "start": "node .", "setup": "mkdir out && cp -r default/* ." }, - "author": "", + "author": "Dakedres", "license": "ISC", "dependencies": { - "jsdom": "^22.1.0" + "jsdom": "^22.1.0", + "mustache": "^4.2.0" }, "type": "module" } diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..2abf398 --- /dev/null +++ b/templates/main.html @@ -0,0 +1,55 @@ + + + +{{title}} - {{index}} + + + + + + +
    + +{{& header}} +
    +
    + +{{#sections}} +
    + +{{#post.images}} +
    +{{/post.images}} +
    + +{{post.source.displayName}} ({{post.source.type}}) {{date.month}}.{{date.day}}.{{date.year}} open +
      + +{{#post.title}} +
    • title {{.}}
    • +{{/post.title}} +{{#categories}} +
    • categories {{#entries}}{{.}}, {{/entries}}{{final}}
    • +{{/categories}} +
    • source {{post.source.hostname}}
    • +{{#occurances}} +
    • lists {{#entries}}{{> occurance-link}} {{/entries}}{{#final}}{{> occurance-link}}{{/final}}
    • +{{/occurances}} + +
    + +
    + +
    +
    +{{/sections}} + +
    +
    +next + +{{& footer}} +
    + + + diff --git a/templates/nav-link.html b/templates/nav-link.html new file mode 100644 index 0000000..4a86728 --- /dev/null +++ b/templates/nav-link.html @@ -0,0 +1 @@ +{{displayName}}{{#errored}} (errored){{/errored}}{{^errored}}{{#empty}} (empty){{/empty}}{{/errored}} diff --git a/templates/nav.html b/templates/nav.html new file mode 100644 index 0000000..13a96c5 --- /dev/null +++ b/templates/nav.html @@ -0,0 +1,30 @@ +
    + +Feeds +
    + +
      + +{{#feeds}} +{{#entries}}{{> nav-link}} {{/entries}}{{#final}}{{> nav-link}}{{/final}} +{{/feeds}} + +
    +
    +
      + +{{#sourceTypes}} +
    • + +{{final.type}}
      +{{#entries}}{{> nav-link}} {{/entries}}{{#final}}{{> nav-link}}{{/final}} + +
    • +{{/sourceTypes}} + +
    + +
    + +
    +
    diff --git a/templates/occurance-link.html b/templates/occurance-link.html new file mode 100644 index 0000000..bbabdd6 --- /dev/null +++ b/templates/occurance-link.html @@ -0,0 +1 @@ +{{list.displayName}} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock old mode 100644 new mode 100755 index 8caed0b..afb41b6 --- a/yarn.lock +++ b/yarn.lock @@ -162,18 +162,16 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -mime-types@^2.1.35: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + nwsapi@^2.2.4: version "2.2.7" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz"