Compare commits
6 Commits
2bfe5dc341
...
d4710c3392
Author | SHA1 | Date |
---|---|---|
Jill | d4710c3392 | |
Jill | c1ebc92daf | |
Jill | ace334e5a3 | |
Jill | 3f3eea8f09 | |
Jill | 2f7d5e0930 | |
Jill | 56bf08de16 |
|
@ -18,7 +18,7 @@
|
|||
pname = "cohost-blogger";
|
||||
inherit (package) version;
|
||||
|
||||
npmDepsHash = "sha256-ixRfoMWVKPomqZJuvKfE2dDrqq7DrTryMCFT37MY/c8=";
|
||||
npmDepsHash = "sha256-pcw+b+IFUG6w07sJ/UN0cPZfvL3oBrSqQBiVhMGquDU=";
|
||||
|
||||
doCheck = true;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,8 +12,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^1.2.4",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"svelte": "^3.54.0",
|
||||
|
@ -36,13 +36,14 @@
|
|||
"rehype-raw": "^6.1.1",
|
||||
"rehype-sanitize": "^5.0.1",
|
||||
"rehype-stringify": "^9.0.3",
|
||||
"remark-breaks": "^3.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-parse": "^10.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"sass": "^1.62.0",
|
||||
"stringify-entities": "^4.0.3",
|
||||
"timeago.js": "^4.0.2",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^4.1.2"
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"video-extensions-list": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,26 +8,6 @@ declare global {
|
|||
// interface Platform {}
|
||||
}
|
||||
|
||||
type ASTMap = RootAST | ElementAST | TextAST
|
||||
|
||||
interface RootAST {
|
||||
type: 'root',
|
||||
position?: any,
|
||||
children: ASTMap[]
|
||||
}
|
||||
interface ElementAST {
|
||||
type: 'element',
|
||||
position?: any,
|
||||
children: ASTMap[],
|
||||
properties: Record<string, any>,
|
||||
tagName: string
|
||||
}
|
||||
interface TextAST {
|
||||
type: 'text',
|
||||
position?: any,
|
||||
value: string
|
||||
}
|
||||
|
||||
interface MarkdownStorageBlock {
|
||||
type: 'markdown',
|
||||
markdown: {
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
:global(img) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +140,7 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="comment-text">
|
||||
{@html renderCommentMarkdown(data.comment.body)}
|
||||
{@html renderCommentMarkdown(data.comment.body, new Date(data.comment.postedAtISO))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
@ -82,11 +82,15 @@
|
|||
}
|
||||
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-position: center;
|
||||
|
||||
&.mask-squircle {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==);
|
||||
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==);
|
||||
}
|
||||
|
||||
&.mask-roundrect {
|
||||
|
@ -101,14 +105,17 @@
|
|||
|
||||
&.mask-egg {
|
||||
mask-image: url('/masks/egg.svg');
|
||||
-webkit-mask-image: url('/masks/egg.svg');
|
||||
}
|
||||
|
||||
&.mask-capsule-big {
|
||||
mask-image: url('/masks/capsule-big.svg');
|
||||
-webkit-mask-image: url('/masks/capsule-big.svg');
|
||||
}
|
||||
|
||||
&.mask-capsule-small {
|
||||
mask-image: url('/masks/capsule-small.svg');
|
||||
-webkit-mask-image: url('/masks/capsule-small.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,6 @@
|
|||
import { stringifyEntities } from 'stringify-entities';
|
||||
import * as fs from 'fs/promises';
|
||||
import config from '$lib/config';
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} properties
|
||||
*/
|
||||
function renderProperties(properties) {
|
||||
if (Object.keys(properties).length === 0) return '';
|
||||
return ' ' + Object.entries(properties)
|
||||
.filter(([k, v]) => k !== '' && v !== '')
|
||||
.map(([k, v]) =>
|
||||
v ? `${k}=${typeof v === 'string' ? `"${stringifyEntities(v)}"` : v.toString()}` : k
|
||||
).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTMap} ast
|
||||
* @returns {string}
|
||||
*/
|
||||
// todo: obliterate from orbit
|
||||
export function renderASTMap(ast) {
|
||||
switch (ast.type) {
|
||||
case 'root':
|
||||
return ast.children.map(c => renderASTMap(c)).join('')
|
||||
case 'element':
|
||||
if (ast.tagName === 'a') {
|
||||
ast.properties.target = '_blank';
|
||||
ast.properties.rel = 'noreferrer noopener';
|
||||
}
|
||||
if (ast.properties.id && ast.properties.id.includes('cohost-blogger-ignore')) return '';
|
||||
return `<${ast.tagName}${renderProperties(ast.properties)}>${ast.children.map(c => renderASTMap(c)).join('')}</${ast.tagName}>`;
|
||||
case 'text':
|
||||
return stringifyEntities(ast.value);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const COHOST_API_URI = 'https://cohost.org/api/v1/trpc/';
|
||||
/**
|
||||
* @param {string} route
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { visit } from "unist-util-visit";
|
||||
import { visit } from 'unist-util-visit';
|
||||
import videoExtensions from 'video-extensions-list';
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
* @param {import('hast').Root} hast
|
||||
*/
|
||||
export function copyImgAltToTitle(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'img' }, (node) => {
|
||||
|
@ -12,24 +13,24 @@ export function copyImgAltToTitle(hast) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
* @param {import('hast').Root} hast
|
||||
*/
|
||||
export function lazyLoadImages(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'img' }, (node) => {
|
||||
node.properties.loading = 'lazy';
|
||||
if (node.properties) node.properties.loading = 'lazy';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
* @param {import('hast').Root} hast
|
||||
*/
|
||||
export function dropCohostBloggerIgnoreBlocks(hast) {
|
||||
visit(hast, { type: 'element' }, (node, index, parent) => {
|
||||
if (parent === null || index === null) return;
|
||||
|
||||
// remove any elements that match the class
|
||||
if (node.properties.className && node.properties.className.includes('cohost-blogger-ignore')) {
|
||||
if (node.properties?.className && node.properties.className.includes('cohost-blogger-ignore')) {
|
||||
parent.children.splice(index, 1);
|
||||
|
||||
// if there's nothing else in the parent, then remove it aswell
|
||||
|
@ -45,7 +46,7 @@ export function dropCohostBloggerIgnoreBlocks(hast) {
|
|||
|
||||
// largely copied from makeIframelyEmbeds
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
* @param {import('hast').Root} hast
|
||||
*/
|
||||
export function makeLazyEmbeds(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'a' }, (node, index, parent) => {
|
||||
|
@ -68,13 +69,19 @@ export function makeLazyEmbeds(hast) {
|
|||
// additionally, GFM autolink literals in their own paragraph are the
|
||||
// only child of their parent node.
|
||||
if (parent.children.length != 1) return;
|
||||
|
||||
// links with no href should be ignored
|
||||
if (!node.properties?.href) return;
|
||||
|
||||
const url = new URL(node.properties.href.toString());
|
||||
|
||||
// plain videos
|
||||
// todo: THIS IS LAZY!!!!
|
||||
if (node.properties?.href.endsWith('.mp4') || node.properties?.href.endsWith('.webm')) {
|
||||
const ext = (url.pathname || '').split('.').pop();
|
||||
if (ext && videoExtensions.includes(ext)) {
|
||||
// render the parent element to fit the video better
|
||||
if (parent.type === 'element') {
|
||||
parent.tagName = 'div';
|
||||
// @ts-ignore
|
||||
parent.properties.style = 'width:100%;display:flex;justify-content:center'
|
||||
}
|
||||
|
||||
|
@ -82,24 +89,26 @@ export function makeLazyEmbeds(hast) {
|
|||
type: 'element',
|
||||
tagName: 'video',
|
||||
properties: {
|
||||
src: node.properties?.href,
|
||||
src: url.href,
|
||||
autoplay: 'true',
|
||||
playsinline: 'true',
|
||||
// todo: what the hell
|
||||
loop: node.properties?.href.includes('autoplay=false') ? 'false' : 'true',
|
||||
style: 'width: 100%;max-width: 600px',
|
||||
// since we're not able to get external metadata, and we don't want to
|
||||
// make _all_ videos autoplay, we have to scan the searchParams for
|
||||
// autoplay=false. this sucks! it's the best we can do
|
||||
loop: (url.searchParams.get('autoplay') !== 'false').toString(),
|
||||
style: 'width:100%;max-width:600px',
|
||||
controls: 'true'
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
// youtube videos
|
||||
} else if (node.properties?.href.startsWith('https://www.youtube.com/')) {
|
||||
} else if (url.hostname === 'www.youtube.com') {
|
||||
// <iframe src="https://www.youtube.com/embed/avNF21NRe10?feature=oembed" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="[NotITG Release] eyes in the water" name="fitvid0" frameborder="0"></iframe>
|
||||
parent.children.splice(index, 1, {
|
||||
type: 'element',
|
||||
tagName: 'iframe',
|
||||
properties: {
|
||||
src: node.properties?.href.replace('/watch?v=', '/embed/'),
|
||||
src: url.href.replace('/watch?v=', '/embed/'),
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
frameborder: 0,
|
||||
style: 'width:100%;aspect-ratio:16/9',
|
||||
|
|
|
@ -9,6 +9,7 @@ import remarkRehype from 'remark-rehype';
|
|||
import rehypeHighlight from 'rehype-highlight'
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import glsl from 'highlight.js/lib/languages/glsl'
|
||||
import deepmerge from 'deepmerge';
|
||||
import { compile } from 'html-to-text';
|
||||
|
@ -17,6 +18,11 @@ const convert = compile({
|
|||
wordwrap: false,
|
||||
});
|
||||
|
||||
// todo: convert to cohost-like age ruleset system?
|
||||
// previous age schemas didn't really affect anything since they were exclusively
|
||||
// sanitization related, but fourth age adds linebreaks, so now i have to
|
||||
// actually bother with it
|
||||
|
||||
const THIRD_AGE_SCHEMA = deepmerge(defaultSchema, {
|
||||
attributes: {
|
||||
"*": ["style"],
|
||||
|
@ -24,15 +30,24 @@ const THIRD_AGE_SCHEMA = deepmerge(defaultSchema, {
|
|||
tagNames: ["video", "audio", "aside"], // consistency with current rules,
|
||||
});
|
||||
|
||||
const LINEBREAK_CUTOFF = new Date("2023-05-10T15:00:00-04:00");
|
||||
|
||||
const externalRel = ['nofollow', 'noopener', 'noreferrer'];
|
||||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @param {boolean} [xhtml]
|
||||
* @param {Date} [date]
|
||||
*/
|
||||
export function renderPostMarkdown(src, xhtml) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
export function renderPostMarkdown(src, xhtml, date) {
|
||||
let stack = unified()
|
||||
.use(remarkParse);
|
||||
|
||||
if ((date || new Date()) > LINEBREAK_CUTOFF) {
|
||||
stack = stack.use(remarkBreaks);
|
||||
}
|
||||
|
||||
return stack
|
||||
.use(remarkGfm, {
|
||||
singleTilde: false,
|
||||
})
|
||||
|
@ -71,8 +86,9 @@ export function renderPostMarkdown(src, xhtml) {
|
|||
|
||||
/**
|
||||
* @param {StorageBlock[]} blocks
|
||||
* @param {Date} [date]
|
||||
*/
|
||||
export function renderPostSummaryMarkdown(blocks) {
|
||||
export function renderPostSummaryMarkdown(blocks, date) {
|
||||
const origBlocks = blocks.filter(block => block.type === 'markdown');
|
||||
const readmoreIndex = origBlocks.findIndex(
|
||||
(block) => block.markdown.content === "---"
|
||||
|
@ -80,16 +96,23 @@ export function renderPostSummaryMarkdown(blocks) {
|
|||
if (readmoreIndex > -1) {
|
||||
origBlocks.splice(readmoreIndex);
|
||||
}
|
||||
return renderPostMarkdown(origBlocks.map(b => b.markdown.content).join('\n\n'));
|
||||
return renderPostMarkdown(origBlocks.map(b => b.markdown.content).join('\n\n'), false, date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @param {Date} [date]
|
||||
* @returns string
|
||||
*/
|
||||
export function renderCommentMarkdown(src) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
export function renderCommentMarkdown(src, date) {
|
||||
let stack = unified()
|
||||
.use(remarkParse);
|
||||
|
||||
if ((date || new Date()) > LINEBREAK_CUTOFF) {
|
||||
stack = stack.use(remarkBreaks);
|
||||
}
|
||||
|
||||
return stack
|
||||
.use(remarkGfm, {
|
||||
singleTilde: false,
|
||||
})
|
||||
|
@ -112,25 +135,28 @@ export function renderCommentMarkdown(src) {
|
|||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @param {Date} [date]
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPlaintext(src) {
|
||||
const renderedBody = renderCommentMarkdown(src);
|
||||
export function renderPlaintext(src, date) {
|
||||
const renderedBody = renderCommentMarkdown(src, date);
|
||||
return convert(renderedBody);
|
||||
}
|
||||
/**
|
||||
* @param {string} src
|
||||
* @param {Date} [date]
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPostPlaintext(src) {
|
||||
const renderedBody = renderPostMarkdown(src);
|
||||
export function renderPostPlaintext(src, date) {
|
||||
const renderedBody = renderPostMarkdown(src, false, date);
|
||||
return convert(renderedBody);
|
||||
}
|
||||
/**
|
||||
* @param {StorageBlock[]} blocks
|
||||
* @param {Date} [date]
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPostSummaryPlaintext(blocks) {
|
||||
const renderedBody = renderPostSummaryMarkdown(blocks);
|
||||
export function renderPostSummaryPlaintext(blocks, date) {
|
||||
const renderedBody = renderPostSummaryMarkdown(blocks, date);
|
||||
return convert(renderedBody);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
.gradient {
|
||||
font-size: 5rem;
|
||||
line-height: 1.0;
|
||||
background: repeating-linear-gradient(98deg, rgba(190,190,190,1) 0%, rgba(190,190,190,1) 10%, rgba(207,207,207,1) 10%, rgba(207,207,207,1) 20%);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bolder;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0.15em;
|
||||
}
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="content">
|
||||
<div class="error">
|
||||
<div class="gradient">
|
||||
<h1>{$page.status}</h1>
|
||||
</div>
|
||||
{$page.error.message}
|
||||
</div>
|
||||
</div>
|
|
@ -30,20 +30,33 @@
|
|||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.location.errored {
|
||||
color: #e95678;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<CommandLine>
|
||||
cd {(new URL($page.url)).pathname}
|
||||
</CommandLine>
|
||||
</header>
|
||||
<div class="content">
|
||||
<header>
|
||||
<CommandLine>
|
||||
cd <span class="location" class:errored={$page.error !== null}>{(new URL($page.url)).pathname}</span>
|
||||
</CommandLine>
|
||||
</header>
|
||||
|
||||
<slot/>
|
||||
<slot/>
|
||||
|
||||
<footer>
|
||||
<div class="inner">
|
||||
<a href="https://cohost.org/{config.handle}/tagged/{config.tag}" target="_blank" rel="noopener noreferrer">view on cohost</a>
|
||||
·
|
||||
<a href={constants.repo} target="_blank" rel="noopener noreferrer">powered by {constants.name}</a>
|
||||
</div>
|
||||
</footer>
|
||||
<footer>
|
||||
<div class="inner">
|
||||
<a href="https://cohost.org/{config.handle}/tagged/{config.tag}" target="_blank" rel="noopener noreferrer">view on cohost</a>
|
||||
·
|
||||
<a href={constants.repo} target="_blank" rel="noopener noreferrer">powered by {constants.name}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
|
@ -206,7 +206,7 @@
|
|||
<header>
|
||||
<h2 class="title">{post.headline}</h2>
|
||||
<div class="post preview excerpt prose">
|
||||
{@html renderPostSummaryMarkdown(post.blocks)}
|
||||
{@html renderPostSummaryMarkdown(post.blocks, post.publishedAt ? new Date(post.publishedAt) : undefined)}
|
||||
</div>
|
||||
</header>
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import Comment from '$lib/Comment.svelte';
|
||||
import { getPostImages, getPostPublishDate, getPostSlug, renderASTMap } from '$lib/cohost';
|
||||
import { getPostImages, getPostPublishDate, getPostSlug } from '$lib/cohost';
|
||||
import { renderPostMarkdown, renderPostSummaryPlaintext } from '$lib/markdown/rendering';
|
||||
import { formatDate } from '$lib/utils';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
@ -33,7 +33,7 @@
|
|||
|
||||
// metadata
|
||||
const canonicalURL = `${config.siteURL}/${getPostSlug(post)}/`;
|
||||
const summary = renderPostSummaryPlaintext(post.blocks);
|
||||
const summary = renderPostSummaryPlaintext(post.blocks, post.publishedAt ? new Date(post.publishedAt) : undefined);
|
||||
const image = getPostImages(post)[0];
|
||||
</script>
|
||||
|
||||
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="post">
|
||||
{@html renderPostMarkdown(post.plainTextBody)}
|
||||
{@html renderPostMarkdown(post.plainTextBody, false, post.publishedAt ? new Date(post.publishedAt) : undefined)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ const xml = (/** @type {Post[]} */ posts) => minifyXML(`
|
|||
.map(post => `
|
||||
<item>
|
||||
<title>${post.headline}</title>
|
||||
<description>${renderPostSummaryPlaintext(post.blocks)}</description>
|
||||
<description>${renderPostSummaryPlaintext(post.blocks, post.publishedAt ? new Date(post.publishedAt) : undefined)}</description>
|
||||
<link>${config.siteURL}/${getPostSlug(post)}/</link>
|
||||
<dc:creator>${post.postingProject.displayName}</dc:creator>
|
||||
<pubDate>${getPostPublishDate(post)?.toUTCString()}</pubDate>
|
||||
|
@ -39,7 +39,7 @@ const xml = (/** @type {Post[]} */ posts) => minifyXML(`
|
|||
`<content url="${getPostImages(post)[0].fileURL}" medium="image" />`
|
||||
: ''}
|
||||
<content:encoded><![CDATA[
|
||||
${renderPostMarkdown(post.plainTextBody, true)}
|
||||
${renderPostMarkdown(post.plainTextBody, true, post.publishedAt ? new Date(post.publishedAt) : undefined)}
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
`)}
|
||||
|
|
Loading…
Reference in New Issue