diff --git a/config/filters/dates.js b/config/filters/dates.js new file mode 100644 index 00000000..ca407e38 --- /dev/null +++ b/config/filters/dates.js @@ -0,0 +1,37 @@ +import { DateTime } from 'luxon' + +export default { + isoDateOnly: (date, separator) => { + let d = new Date(date) + let month = '' + (d.getMonth() + 1) + let day = '' + d.getDate() + let year = d.getFullYear() + + if (month.length < 2) month = '0' + month + if (day.length < 2) day = '0' + day + + return [year, month, day].join(separator) + }, + oldPost: (date) => { + return DateTime.now().diff(DateTime.fromJSDate(new Date(date)), 'years').years > 3; + }, + stringToRFC822Date: (dateString) => { + const addLeadingZero = (num) => { + num = num.toString(); + while (num.length < 2) num = "0" + num; + return num; + } + const dayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const timeStamp = Date.parse(dateString); + const date = new Date(timeStamp); + const day = dayStrings[date.getDay()]; + const dayNumber = addLeadingZero(date.getDate()); + const month = monthStrings[date.getMonth()]; + const year = date.getFullYear(); + const time = `${addLeadingZero(date.getHours())}:${addLeadingZero(date.getMinutes())}:00`; + const timezone = date.getTimezoneOffset() === 0 ? "GMT" : "PT"; + + return `${day}, ${dayNumber} ${month} ${year} ${time} ${timezone}`; + } +} \ No newline at end of file diff --git a/config/filters/feeds.js b/config/filters/feeds.js new file mode 100644 index 00000000..aaa6d99c --- /dev/null +++ b/config/filters/feeds.js @@ -0,0 +1,80 @@ +import { URL } from 'url' +import markdownIt from 'markdown-it' +import markdownItAnchor from 'markdown-it-anchor' +import markdownItFootnote from 'markdown-it-footnote' +import sanitizeHtml from 'sanitize-html' + +const BASE_URL = 'https://coryd.dev' + +export default { + normalizeEntries: (entries, limit) => { + const posts = [] + const mdGenerator = () => { + const md = markdownIt({ html: true, linkify: true }) + + md.use(markdownItAnchor, { + level: [1, 2], + permalink: markdownItAnchor.permalink.headerLink({ + safariReaderFix: true + }) + }) + md.use(markdownItFootnote) + md.renderer.rules.footnote_ref = (tokens, idx) => { + const id = tokens[idx].meta.id + 1 + return `${id}` + } + md.renderer.rules.footnote_block_open = () => ( + '
\n
\n
    \n' + ) + md.renderer.rules.footnote_open = (tokens, idx) => { + const id = tokens[idx].meta.id + 1 + return `
  1. ` + } + md.renderer.rules.footnote_anchor = () => '' + + return md + } + const entryData = limit ? entries.slice(0, limit) : entries + + entryData.forEach((entry) => { + const md = mdGenerator() + const dateKey = Object.keys(entry).find(key => key.includes('date')) + let { artist, authors, backdrop, content, description, image, link, rating, slug, title, url, tags, type } = entry + const feedNote = '

    This is a full text feed, but not all content can be rendered perfectly within the feed. If something looks off, feel free to visit my site for the original post.

    ' + const processedEntry = { title: title.trim(), date: new Date(entry[dateKey]), content: description } + + if (url?.includes('http')) processedEntry['url'] = url + if (!url?.includes('http')) processedEntry['url'] = new URL(url, BASE_URL).toString() + if (slug) processedEntry['url'] = new URL(slug, BASE_URL).toString() + if (link) { + processedEntry['title'] = `${title} via ${authors['name']}` + processedEntry['url'] = link, + processedEntry['author'] = { + name: authors['name'], + url: authors['url'], + mastodon: authors?.['mastodon'] || '', + rss: authors?.['rss_feed'] || '' + } + } + if (description) processedEntry['excerpt'] = description + if (['book', 'movie', 'link'].includes(type)) processedEntry['excerpt'] = sanitizeHtml(`${md.render(description)}`) + if (slug && content) processedEntry['excerpt'] = sanitizeHtml(`${md.render(content)}${feedNote}`, { + disallowedTagsMode: 'completelyDiscard' + }) + + processedEntry['image'] = backdrop || image + + if (rating) processedEntry['rating'] = rating + if (tags) processedEntry['tags'] = tags + if (type === 'album-release') { + if (artist) processedEntry['title'] = `${title} by ${artist}` + processedEntry['excerpt'] = 'Check out the new release!' + processedEntry['content'] = 'Check out the new release!' + } + + if (entry) posts.push(processedEntry) + }) + + return posts + } +} \ No newline at end of file diff --git a/config/filters/general.js b/config/filters/general.js new file mode 100644 index 00000000..cb394bc3 --- /dev/null +++ b/config/filters/general.js @@ -0,0 +1,20 @@ +import sanitizeHtml from 'sanitize-html' +import { shuffleArray, sanitizeMediaString } from '../utilities/index.js' + +const BASE_URL = 'https://coryd.dev' + +export default { + encodeAmp: (string) => { + if (!string) return + const pattern = /&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/g + const replacement = '&' + return string.replace(pattern, replacement) + }, + formatNumber: (number) => number.toLocaleString('en-US'), + shuffleArray, + sanitizeMediaString, + sanitizeHtml: (html) => sanitizeHtml(html, { + textFilter: (text) => text.replace(/"/g, '') + }), + absoluteUrl: (url) => (new URL(url, BASE_URL)).toString(), +} \ No newline at end of file diff --git a/config/filters/index.js b/config/filters/index.js index 4fe7b55c..7b2f1114 100644 --- a/config/filters/index.js +++ b/config/filters/index.js @@ -1,279 +1,15 @@ -import { DateTime } from 'luxon' -import { URL } from 'url' -import markdownIt from 'markdown-it' -import markdownItAnchor from 'markdown-it-anchor' -import markdownItFootnote from 'markdown-it-footnote' -import sanitizeHtml from 'sanitize-html' -import { shuffleArray, sanitizeMediaString } from '../utilities/index.js' - -const BASE_URL = 'https://coryd.dev' +import dates from './dates.js' +import feeds from './feeds.js' +import general from './general.js' +import media from './media.js' +import navigation from './navigation.js' +import posts from './posts.js' export default { - // general - encodeAmp: (string) => { - if (!string) return - const pattern = /&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/g - const replacement = '&' - return string.replace(pattern, replacement) - }, - formatNumber: (number) => number.toLocaleString('en-US'), - shuffleArray, - sanitizeMediaString, - sanitizeHtml: (html) => sanitizeHtml(html, { - textFilter: (text) => text.replace(/"/g, '') - }), - - // navigation - isLinkActive: (category, page) => page.includes(category) && page.split('/').filter(a => a !== '').length <= 1, - - // posts - filterByPostType: (posts, postType) => { - if (postType === 'featured') return shuffleArray(posts.filter(post => post.featured === true)).slice(0, 3) - return posts.slice(0, 5) - }, - - // watching - featuredWatching: (watching, count) => { - const data = [...watching] - return shuffleArray(data).slice(0, count) - }, - - // dates - isoDateOnly: (date, separator) => { - let d = new Date(date) - let month = '' + (d.getMonth() + 1) - let day = '' + d.getDate() - let year = d.getFullYear() - - if (month.length < 2) month = '0' + month - if (day.length < 2) day = '0' + day - - return [year, month, day].join(separator) - }, - oldPost: (date) => { - return DateTime.now().diff(DateTime.fromJSDate(new Date(date)), 'years').years > 3; - }, - stringToRFC822Date: (dateString) => { - const addLeadingZero = (num) => { - num = num.toString(); - while (num.length < 2) num = "0" + num; - return num; - } - const dayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const timeStamp = Date.parse(dateString); - const date = new Date(timeStamp); - const day = dayStrings[date.getDay()]; - const dayNumber = addLeadingZero(date.getDate()); - const month = monthStrings[date.getMonth()]; - const year = date.getFullYear(); - const time = `${addLeadingZero(date.getHours())}:${addLeadingZero(date.getMinutes())}:00`; - const timezone = date.getTimezoneOffset() === 0 ? "GMT" : "PT"; - - return `${day}, ${dayNumber} ${month} ${year} ${time} ${timezone}`; - }, - - // links - absoluteUrl: (url) => (new URL(url, BASE_URL)).toString(), - - // feeds - normalizeEntries: (entries, limit) => { - const posts = [] - const mdGenerator = () => { - const md = markdownIt({ html: true, linkify: true }) - - md.use(markdownItAnchor, { - level: [1, 2], - permalink: markdownItAnchor.permalink.headerLink({ - safariReaderFix: true - }) - }) - md.use(markdownItFootnote) - md.renderer.rules.footnote_ref = (tokens, idx) => { - const id = tokens[idx].meta.id + 1 - return `${id}` - } - md.renderer.rules.footnote_block_open = () => ( - '
    \n
    \n
      \n' - ) - md.renderer.rules.footnote_open = (tokens, idx) => { - const id = tokens[idx].meta.id + 1 - return `
    1. ` - } - md.renderer.rules.footnote_anchor = () => '' - - return md - } - const entryData = limit ? entries.slice(0, limit) : entries - - entryData.forEach((entry) => { - const md = mdGenerator() - const dateKey = Object.keys(entry).find(key => key.includes('date')) - let { artist, authors, backdrop, content, description, image, link, rating, slug, title, url, tags, type } = entry - const feedNote = '

      This is a full text feed, but not all content can be rendered perfectly within the feed. If something looks off, feel free to visit my site for the original post.

      ' - const processedEntry = { title: title.trim(), date: new Date(entry[dateKey]), content: description } - - if (url?.includes('http')) processedEntry['url'] = url - if (!url?.includes('http')) processedEntry['url'] = new URL(url, BASE_URL).toString() - if (slug) processedEntry['url'] = new URL(slug, BASE_URL).toString() - if (link) { - processedEntry['title'] = `${title} via ${authors['name']}` - processedEntry['url'] = link, - processedEntry['author'] = { - name: authors['name'], - url: authors['url'], - mastodon: authors?.['mastodon'] || '', - rss: authors?.['rss_feed'] || '' - } - } - if (description) processedEntry['excerpt'] = description - if (['book', 'movie', 'link'].includes(type)) processedEntry['excerpt'] = sanitizeHtml(`${md.render(description)}`) - if (slug && content) processedEntry['excerpt'] = sanitizeHtml(`${md.render(content)}${feedNote}`, { - disallowedTagsMode: 'completelyDiscard' - }) - - processedEntry['image'] = backdrop || image - - if (rating) processedEntry['rating'] = rating - if (tags) processedEntry['tags'] = tags - if (type === 'album-release') { - if (artist) processedEntry['title'] = `${title} by ${artist}` - processedEntry['excerpt'] = 'Check out the new release!' - processedEntry['content'] = 'Check out the new release!' - } - - if (entry) posts.push(processedEntry) - }) - - return posts - }, - - // media - normalizeMedia: (media, limit) => { - const mediaData = limit ? media.slice(0, limit) : media - return mediaData.map((item) => { - let normalized = { - image: item['image'], - url: item['url'], - type: item['type'] - } - - switch (item['type']) { - case 'artist': - normalized.title = item['title'] - normalized.alt = `${item['plays']} plays of ${item['title']}` - normalized.subtext = `${item['plays']} plays` - break - case 'album': - normalized.title = item['title'] - normalized.alt = `${item['title']} by ${item['artist']}` - normalized.subtext = `${item['artist']}` - break - case 'album-release': - normalized.title = item['title'] - normalized.alt = `${item['title']} by ${item['artist']}` - normalized.subtext = `${item['artist']} / ${item['date']}` - break - case 'movie': - normalized.title = item['title'] - normalized.alt = item['title'] - normalized.rating = item['rating'] - normalized.favorite = item['favorite'] - normalized.subtext = item.rating ? `${item['rating']} (${item['year']})` : `(${item['year']})` - break - case 'book': - normalized.title = `${item['title']} by ${item['author']}` - if (item['rating']) { - normalized.rating = item['rating'] - normalized.subtext = item['rating'] - } - break - case 'tv': - case 'tv-range': - normalized.title = item['name'] - normalized.alt = `${item['subtext']} ${item['type'] === 'tv' ? 'of' : 'from'} ${item['name']}` - normalized.subtext = item['subtext'] - break - } - - return normalized - }) - }, - calculatePlayPercentage: (plays, mostPlayed) => `${plays/mostPlayed * 100}%`, - bookStatus: (books, status) => books.filter(book => book.status === status), - bookFavorites: (books) => books.filter(book => book.favorite === true), - bookYearLinks: (years) => years.sort((a, b) => b.value - a.value).map((year, index) => { - const separator = index < years.length - 1 ? ' / ' : ''; - return `${year.value}${separator}`; - }).join(''), - bookSortDescending: (books) => books.filter(book => !isNaN(DateTime.fromISO(book.date).toMillis())).sort((a, b) => { - const dateA = DateTime.fromISO(a.date) - const dateB = DateTime.fromISO(b.date) - return dateB - dateA - }), - bookFinishedYear: (books, year) => books.filter(book => { - if (book.status === 'finished' && book.date) return parseInt(book.date.split('-')[0]) === year - return '' - }), - currentBookCount: (books) => { - const year = DateTime.now().year - return books.filter(book => { - if (book.status === 'finished' && book.date) return parseInt(book.date.split('-')[0]) === year - return '' - }).length - }, - getLastWatched: (show) => show?.['episodes'][show['episodes']?.length - 1]?.['last_watched_at'], - sortByPlaysDescending: (data, key) => data.sort((a, b) => b[key] - a[key]), - genreStrings: (genres, key) => genres.map(genre => genre[key]), - mediaLinks: (data, type, count = 10) => { - if (!data || !type) return '' - - const dataSlice = data.slice(0, count) - - if (dataSlice.length === 0) return null - if (dataSlice.length === 1) { - const item = dataSlice[0] - if (type === 'genre') { - return `${item}` - } else if (type === 'artist') { - return `${item['name_string']}` - } else if (type === 'book') { - return `${item['title']}` - } else if (type === 'movie') { - return `${item['title']}` - } - } - - const allButLast = dataSlice.slice(0, -1).map(item => { - if (type === 'genre') { - return `${item}` - } else if (type === 'artist') { - return `${item['name_string']}` - } else if (type === 'book') { - return `${item['title']}` - } else if (type === 'movie') { - return `${item['title']}` - } - }).join(', ') - - let last - const lastItem = dataSlice[dataSlice.length - 1] - - if (type === 'genre') { - last = `${lastItem}` - } else if (type === 'artist') { - last = `${lastItem['name_string']}` - } else if (type === 'book') { - last = `${lastItem['title']}` - } else if (type === 'movie') { - last = `${lastItem['title']}` - } - return `${allButLast} and ${last}` - }, - formatVenue: (venue) => venue.split(',')[0].trim(), - lastWatchedEpisode: (episodes) => { - if (!episodes.length) return - const sortedEpisodes = episodes.sort((a, b) => new Date(a.last_watched_at) - new Date(b.last_watched_at)) - return `S${sortedEpisodes[sortedEpisodes.length - 1]['season_number']}E${sortedEpisodes[sortedEpisodes.length - 1]['episode_number']}` - } + ...dates, + ...feeds, + ...general, + ...media, + ...navigation, + ...posts } \ No newline at end of file diff --git a/config/filters/media.js b/config/filters/media.js new file mode 100644 index 00000000..d077eb5b --- /dev/null +++ b/config/filters/media.js @@ -0,0 +1,136 @@ +import { DateTime } from 'luxon' +import { shuffleArray, sanitizeMediaString } from '../utilities/index.js' + +export default { + featuredWatching: (watching, count) => { + const data = [...watching] + return shuffleArray(data).slice(0, count) + }, + normalizeMedia: (media, limit) => { + const mediaData = limit ? media.slice(0, limit) : media + return mediaData.map((item) => { + let normalized = { + image: item['image'], + url: item['url'], + type: item['type'] + } + + switch (item['type']) { + case 'artist': + normalized.title = item['title'] + normalized.alt = `${item['plays']} plays of ${item['title']}` + normalized.subtext = `${item['plays']} plays` + break + case 'album': + normalized.title = item['title'] + normalized.alt = `${item['title']} by ${item['artist']}` + normalized.subtext = `${item['artist']}` + break + case 'album-release': + normalized.title = item['title'] + normalized.alt = `${item['title']} by ${item['artist']}` + normalized.subtext = `${item['artist']} / ${item['date']}` + break + case 'movie': + normalized.title = item['title'] + normalized.alt = item['title'] + normalized.rating = item['rating'] + normalized.favorite = item['favorite'] + normalized.subtext = item.rating ? `${item['rating']} (${item['year']})` : `(${item['year']})` + break + case 'book': + normalized.title = `${item['title']} by ${item['author']}` + if (item['rating']) { + normalized.rating = item['rating'] + normalized.subtext = item['rating'] + } + break + case 'tv': + case 'tv-range': + normalized.title = item['name'] + normalized.alt = `${item['subtext']} ${item['type'] === 'tv' ? 'of' : 'from'} ${item['name']}` + normalized.subtext = item['subtext'] + break + } + + return normalized + }) + }, + calculatePlayPercentage: (plays, mostPlayed) => `${plays/mostPlayed * 100}%`, + bookStatus: (books, status) => books.filter(book => book.status === status), + bookFavorites: (books) => books.filter(book => book.favorite === true), + bookYearLinks: (years) => years.sort((a, b) => b.value - a.value).map((year, index) => { + const separator = index < years.length - 1 ? ' / ' : ''; + return `${year.value}${separator}`; + }).join(''), + bookSortDescending: (books) => books.filter(book => !isNaN(DateTime.fromISO(book.date).toMillis())).sort((a, b) => { + const dateA = DateTime.fromISO(a.date) + const dateB = DateTime.fromISO(b.date) + return dateB - dateA + }), + bookFinishedYear: (books, year) => books.filter(book => { + if (book.status === 'finished' && book.date) return parseInt(book.date.split('-')[0]) === year + return '' + }), + currentBookCount: (books) => { + const year = DateTime.now().year + return books.filter(book => { + if (book.status === 'finished' && book.date) return parseInt(book.date.split('-')[0]) === year + return '' + }).length + }, + getLastWatched: (show) => show?.['episodes'][show['episodes']?.length - 1]?.['last_watched_at'], + sortByPlaysDescending: (data, key) => data.sort((a, b) => b[key] - a[key]), + genreStrings: (genres, key) => genres.map(genre => genre[key]), + mediaLinks: (data, type, count = 10) => { + if (!data || !type) return '' + + const dataSlice = data.slice(0, count) + + if (dataSlice.length === 0) return null + if (dataSlice.length === 1) { + const item = dataSlice[0] + if (type === 'genre') { + return `${item}` + } else if (type === 'artist') { + return `${item['name_string']}` + } else if (type === 'book') { + return `${item['title']}` + } else if (type === 'movie') { + return `${item['title']}` + } + } + + const allButLast = dataSlice.slice(0, -1).map(item => { + if (type === 'genre') { + return `${item}` + } else if (type === 'artist') { + return `${item['name_string']}` + } else if (type === 'book') { + return `${item['title']}` + } else if (type === 'movie') { + return `${item['title']}` + } + }).join(', ') + + let last + const lastItem = dataSlice[dataSlice.length - 1] + + if (type === 'genre') { + last = `${lastItem}` + } else if (type === 'artist') { + last = `${lastItem['name_string']}` + } else if (type === 'book') { + last = `${lastItem['title']}` + } else if (type === 'movie') { + last = `${lastItem['title']}` + } + return `${allButLast} and ${last}` + }, + formatVenue: (venue) => venue.split(',')[0].trim(), + lastWatchedEpisode: (episodes) => { + if (!episodes.length) return + const sortedEpisodes = episodes.sort((a, b) => new Date(a.last_watched_at) - new Date(b.last_watched_at)) + return `S${sortedEpisodes[sortedEpisodes.length - 1]['season_number']}E${sortedEpisodes[sortedEpisodes.length - 1]['episode_number']}` + } +} \ No newline at end of file diff --git a/config/filters/navigation.js b/config/filters/navigation.js new file mode 100644 index 00000000..1b139039 --- /dev/null +++ b/config/filters/navigation.js @@ -0,0 +1,3 @@ +export default { + isLinkActive: (category, page) => page.includes(category) && page.split('/').filter(a => a !== '').length <= 1 +} \ No newline at end of file diff --git a/config/filters/posts.js b/config/filters/posts.js new file mode 100644 index 00000000..5d4e62b8 --- /dev/null +++ b/config/filters/posts.js @@ -0,0 +1,8 @@ +import { shuffleArray } from '../utilities/index.js' + +export default { + filterByPostType: (posts, postType) => { + if (postType === 'featured') return shuffleArray(posts.filter(post => post.featured === true)).slice(0, 3) + return posts.slice(0, 5) + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 62c7a410..e86105d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coryd.dev", - "version": "24.6.5", + "version": "24.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coryd.dev", - "version": "24.6.5", + "version": "24.7.0", "license": "MIT", "dependencies": { "@cdransf/api-text": "^1.5.0", @@ -1743,9 +1743,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.14", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.14.tgz", - "integrity": "sha512-bEfPECb3fJ15eaDnu9LEJ2vPGD6W1vt7vZleSVyFhYuMIKm3vz/g9lt7IvEzgdwj58RjbPKUF2rXTCN/UW47tQ==", + "version": "1.5.15", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz", + "integrity": "sha512-Z4rIDoImwEJW+YYKnPul4DzqsWVqYetYVN3XqDmRpgV0mjz0hYTaeeh+8/9CL1bk3AHYmF4freW/NTiVoXA2gA==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 431bca06..a30943b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coryd.dev", - "version": "24.6.5", + "version": "24.7.0", "description": "The source for my personal site. Built using 11ty (and other tools).", "type": "module", "scripts": {