Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

render external resource OG #23

Merged
merged 18 commits into from Nov 18, 2022
13 changes: 7 additions & 6 deletions package.json
Expand Up @@ -14,19 +14,20 @@
},
"devDependencies": {
"@types/marked": "4.0.1",
"@types/node": "^18.7.16",
"@types/node": "18.11.9",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"eslint": "8.27.0",
"eslint-config-preact": "1.3.0",
"wmr": "3.8.0",
"typescript": "4.9.3"
"typescript": "4.9.3",
"wmr": "3.8.0"
},
"dependencies": {
"hoofd": "^1.2.2",
"marked": "4.0.10",
"preact": "^10.11.0",
"preact-iso": "2.3.0"
"jsdom": "^20.0.1",
"marked": "4.1.0",
"preact": "10.11.0",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preact is broken since 10.11.1 due to SignalLike changes...
I can't think of a good way to do it, so downgrade once and use it
image

preactjs/preact#3747

"preact-iso": "^2.3.1"
},
"author": "takurinton",
"eslintConfig": {
Expand Down
12 changes: 12 additions & 0 deletions public/contents/112.md
@@ -0,0 +1,12 @@
---
id: 112
title: og
description: og について
created_at: 2022-10-11
---

hogehoge

@og[https://blog.takurinton.dev/post/111]

@twitter[https://twitter.com/takurinton/status/1579390918603706368]
364 changes: 363 additions & 1 deletion public/contents/posts.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions public/prerender.tsx
Expand Up @@ -2,7 +2,18 @@ import { prerender as ssr } from "preact-iso";
import { VNode } from "preact";
import { toStatic } from "hoofd/preact";

let initialized = false;
async function init() {
// eslint-disable-next-line no-undef
globalThis.DOMParser = new (require("jsdom").JSDOM)("").window.DOMParser;
}

export async function prerender(vnode: VNode) {
if (!initialized) {
initialized = true;
await init();
}

const res = await ssr(vnode);

const head = toStatic();
Expand Down
32 changes: 15 additions & 17 deletions src/hooks/usePost.ts
Expand Up @@ -33,8 +33,20 @@ export function usePost(id: string): Response {
CACHE.set(url, post);
post.then(
(value: FetchPost) => {
post.value = value;
setPost(post);
const md = new MarkdownInit(value.mdStr);
const title = md.getTitle();
const createdAt = md.getCreatedAt();
const description = `${title} について書きました。`;
markdown(md.getContent()).then((content) => {
post.value = {
id,
title,
content,
description,
createdAt,
};
setPost(post);
});
},
(error: any) => {
post.error = error;
Expand All @@ -43,21 +55,7 @@ export function usePost(id: string): Response {
);
}

if (post.value !== undefined) {
const md = new MarkdownInit(post.value.mdStr);
const title = md.getTitle();
const content = markdown(md.getContent());
const createdAt = md.getCreatedAt();
const description = `${title} について書きました。`;

return {
id,
title,
description,
createdAt,
content,
};
}
if (post.value !== undefined) return post.value;
if (post.error !== undefined) throw new Error(post.error);
throw post;
}
114 changes: 114 additions & 0 deletions src/md/getHtml.ts
@@ -0,0 +1,114 @@
const getMetaTags = (html, link) => {
const description = html.getElementsByName("description")[0];

const ogImage = html.querySelector('meta[property="og:image"]');
const ogImageContent = ogImage ? ogImage.content : { content: "" };

const domain = link.match(/^https?:\/{2,}(.*?)(?:\/|\?|#|$)/)[1];
let image;
if (ogImage === undefined) {
image = "";
} else if (/^https?:\/\//.test(ogImageContent)) {
const file = ogImageContent;
const fileLink = file.match(/^https?:\/{2,}(.*?)(?:\/|\?|#|$)/);

if (fileLink === null) image = `https://${domain}${file.slice(7)}`;
else if (fileLink[1] !== domain) {
const filePathSplit = file.split("/")[3];
image = `https://${fileLink[1]}/${filePathSplit}`;
}
} else {
const file = ogImageContent;
const fileLink = file.match(/^https?:\/{2,}(.*?)(?:\/|\?|#|$)/);
if (fileLink === null) image = `https://${domain}${file.slice(7)}`;
else {
const filePathSplit = file.split("/").slice(3).join("/");
image = `https://${domain}/${filePathSplit}`;
}
}

return {
title: html.title,
description: description === undefined ? "" : description.content,
image: image ?? "",
};
};

const fetchExternalHtml = async (url) => {
const res = await fetch(url);
const html = await res.text();
return html;
};

export const setHtml = async (url, id) => {
const htmlString = await fetchExternalHtml(url);
const html = new DOMParser().parseFromString(htmlString, "text/html");
const { title, description, image } = getMetaTags(html, url);

if (typeof window !== "undefined") {
const el = document.getElementById(id);
if (el === null) return;

el.innerHTML = `
<div class="og">
<a href="${url}" target="_blank" >
<div class="left">
<img src="${image}" alt="${title}" />
</div>
<div class="right">
<h1>${title}</h1>
<p class="description">${description}</p>
<p class="link">${url}</p>
</div>
</a>
</div>`;

const style = Object.assign(document.createElement("style"), {
innerHTML: `
.og > a {
border: 1px gray solid;
border-radius: 5px;
width: 80%;
padding: 10px;
display: flex;
text-decoration: none;
color: #222222;
}
.left {
height: 100px;
width: 100px;
text-align: center;
padding-right: 40px;
}
.left > img {
height: 100px;
width: 100px;
}
.right {
display: block;
overflow: hidden;
}
.right > h1,
.right > p,
.right > a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-overflow: ellipsis;
}
.right > h1 {
height: 50px;
margin: 0;
}
.right > p {
margin: 0;
}
.link {
color: gray;
}
`,
});

document.head.appendChild(style);
}
};
141 changes: 6 additions & 135 deletions src/md/markdown.ts
@@ -1,137 +1,8 @@
import { marked } from "marked";
import { renderer } from "./renderer";
import { plugin } from "./plugin";

const twitter = {
name: "twitter",
level: "block",
start(src) {
return src.match(/^@twitter\[.*\]$/)?.index;
},
// eslint-disable-next-line no-unused-vars
tokenizer(src, tokens) {
const rule = /^@twitter\[(.*)\]/;
const match = rule.exec(src);
if (match !== null) {
const token = {
type: "twitter",
raw: match[0],
text: match[1],
id: match[1].split("/").pop(),
tokens: [],
};
// @ts-ignore
this.lexer.inline(token.text, token.tokens);
return token;
}
},
renderer(token) {
return `<blockquote class="twitter-tweet" id="${token.id}"></blockquote>`;
},
};

// const og = {
// name: "og",
// level: "block",
// start(src) {
// return src.match(/^@og/)?.index;
// },
// tokenizer(src, tokens) {
// const rule = /^@og\[.*\]/;
// const match = rule.exec(src);

// const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(src);
// // console.log(link);

// if (match !== null) {
// const token = {
// type: "og",
// raw: match[0],
// text: match[0].trim(),
// tokens: [],
// };
// this.lexer.inline(token.text, token.tokens);
// return token;
// }
// },
// renderer(token) {
// return `hoge`;
// },
// };

const renderer = {
twitter(type, raw, text) {
const tweetId = text.split("/").pop();
return `
<blockquote class="twitter-tweet" id="${tweetId}"></blockquote>
`;
},
heading(text, level) {
if (level === 1) {
return `<h${level} class="h${level}" style="border-bottom: 2px solid #ff69b4; padding-bottom: 5px">${text}</h${level}>`;
}
return `<h${level} class="h${level}">${text}</h${level}>`;
},
table(header, body) {
return `
<table class="table" border="1" width="100%">
${header}
${body}
</table>
`;
},
list(body: string, ordered: boolean, start: number) {
if (ordered) {
return `
<ul style='padding-left: ${start}'>
${body}
</ul>
`;
}
return `
<ul>
${body}
</ul>
`;
},
image(href, title, text) {
return `
<img src=${href} alt=${text} class="content-img ${title ?? ""}" />
`;
},
paragraph(text) {
return `
<p class="p">${text}</p>
`;
},
link(href, title, text) {
// const isTwitterLink = /^https:\/\/twitter\.com\/\w+\/status\/\d+/.test(
// href
// );

// if (isTwitterLink) {
// const tweetId = href.split("/").pop();
// return `
// <blockquote class="twitter-tweet" id="${tweetId}"></blockquote>
// `;
// }

return `
<a href="${href}" target="_blank" class=${title}>${text}</a>
`;
},
blockquote(quote) {
return `<blockquote style="border-left:3px solid gray;margin:0 0 0 10px;padding-left:20px;color:gray;">${quote}</blockquote>`;
},
};

// TODO: rintonmd
// https://github.com/takurinton/rintonmd
export const markdown = (md: string): string => {
marked.use({
extensions: [
twitter,
// og
],
});
marked.use({ renderer });
return marked.parse(md);
};
export async function markdown(md: string) {
marked.use({ renderer, ...plugin });
return await marked.parse(md);
}