import * as URL from 'node:url'; import clip from './utils/clip.js'; import cleanupTitle from './utils/cleanup-title.js'; import { decode as decodeHtml } from 'html-entities'; import { head, scpaping } from './utils/got.js'; export default async (url, lang = null) => { if (lang && !lang.match(/^[\w-]+(\s*,\s*[\w-]+)*$/)) lang = null; const res = await scpaping(url.href, { lang: lang || undefined }); const $ = res.$; const twitterCard = $('meta[property="twitter:card"]').attr('content'); let title = $('meta[property="og:title"]').attr('content') || $('meta[property="twitter:title"]').attr('content') || $('title').text(); if (title === undefined || title === null) { return null; } title = clip(decodeHtml(title), 100); let image = $('meta[property="og:image"]').attr('content') || $('meta[property="twitter:image"]').attr('content') || $('link[rel="image_src"]').attr('href') || $('link[rel="apple-touch-icon"]').attr('href') || $('link[rel="apple-touch-icon image_src"]').attr('href'); image = image ? URL.resolve(url.href, image) : null; const playerUrl = (twitterCard !== 'summary_large_image' && $('meta[property="twitter:player"]').attr('content')) || (twitterCard !== 'summary_large_image' && $('meta[name="twitter:player"]').attr('content')) || $('meta[property="og:video"]').attr('content') || $('meta[property="og:video:secure_url"]').attr('content') || $('meta[property="og:video:url"]').attr('content'); const playerWidth = parseInt($('meta[property="twitter:player:width"]').attr('content') || $('meta[name="twitter:player:width"]').attr('content') || $('meta[property="og:video:width"]').attr('content') || ''); const playerHeight = parseInt($('meta[property="twitter:player:height"]').attr('content') || $('meta[name="twitter:player:height"]').attr('content') || $('meta[property="og:video:height"]').attr('content') || ''); let description = $('meta[property="og:description"]').attr('content') || $('meta[property="twitter:description"]').attr('content') || $('meta[name="description"]').attr('content'); description = description ? clip(decodeHtml(description), 300) : null; if (title === description) { description = null; } let siteName = $('meta[property="og:site_name"]').attr('content') || $('meta[name="application-name"]').attr('content') || url.hostname; siteName = siteName ? decodeHtml(siteName) : null; const favicon = $('link[rel="shortcut icon"]').attr('href') || $('link[rel="icon"]').attr('href') || '/favicon.ico'; const sensitive = $('.tweet').attr('data-possibly-sensitive') === 'true'; const find = async (path) => { const target = URL.resolve(url.href, path); try { await head(target); return target; } catch (e) { return null; } }; // 相対的なURL (ex. test) を絶対的 (ex. /test) に変換 const toAbsolute = (relativeURLString) => { const relativeURL = URL.parse(relativeURLString); const isAbsolute = relativeURL.slashes || relativeURL.path !== null && relativeURL.path[0] === '/'; // 既に絶対的なら、即座に値を返却 if (isAbsolute) { return relativeURLString; } // スラッシュを付けて返却 return '/' + relativeURLString; }; const icon = await find(favicon) || // 相対指定を絶対指定に変換し再試行 await find(toAbsolute(favicon)) || null; // Clean up the title title = cleanupTitle(title, siteName); if (title === '') { title = siteName; } return { title: title || null, icon: icon || null, description: description || null, thumbnail: image || null, player: { url: playerUrl || null, width: Number.isNaN(playerWidth) ? null : playerWidth, height: Number.isNaN(playerHeight) ? null : playerHeight }, sitename: siteName || null, sensitive, }; };