mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-15 23:19:48 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a6afd576f | |||
| ebc74a10ad | |||
| ea84f21439 | |||
| 0b9e51d6cc |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.71.0"
|
"version": "1.73.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.73.0 - 2026-03-18
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-danger-x-to-markdown`: add video media support for X articles with poster image and video link rendering
|
||||||
|
|
||||||
|
## 1.72.0 - 2026-03-18
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-danger-x-to-markdown`: add MARKDOWN entity support for rendering embedded markdown/code blocks in X articles
|
||||||
|
|
||||||
## 1.71.0 - 2026-03-17
|
## 1.71.0 - 2026-03-17
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.73.0 - 2026-03-18
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-danger-x-to-markdown`:支持 X 文章中的视频媒体,渲染封面图和视频链接
|
||||||
|
|
||||||
|
## 1.72.0 - 2026-03-18
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-danger-x-to-markdown`:支持渲染 X 文章中嵌入的 MARKDOWN 实体(代码块等)
|
||||||
|
|
||||||
## 1.71.0 - 2026-03-17
|
## 1.71.0 - 2026-03-17
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.71.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.73.0**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { formatArticleMarkdown } from "./markdown.js";
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders MARKDOWN entities from atomic blocks", () => {
|
||||||
|
const article = {
|
||||||
|
title: "Atomic Markdown Example",
|
||||||
|
content_state: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "Before the snippet.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "atomic",
|
||||||
|
text: " ",
|
||||||
|
entityRanges: [{ key: 0, offset: 0, length: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "After the snippet.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entityMap: {
|
||||||
|
"0": {
|
||||||
|
key: "5",
|
||||||
|
value: {
|
||||||
|
type: "MARKDOWN",
|
||||||
|
mutability: "Mutable",
|
||||||
|
data: {
|
||||||
|
markdown: "```python\nprint('hello from x article')\n```\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
expect(markdown).toContain("Before the snippet.");
|
||||||
|
expect(markdown).toContain("```python\nprint('hello from x article')\n```");
|
||||||
|
expect(markdown).toContain("After the snippet.");
|
||||||
|
expect(markdown).toBe(`# Atomic Markdown Example
|
||||||
|
|
||||||
|
Before the snippet.
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
print('hello from x article')
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
After the snippet.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders article video media as poster plus video link", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/123/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/123/vid/avc1/720x720/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Video Example",
|
||||||
|
content_state: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "Intro text.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "atomic",
|
||||||
|
text: " ",
|
||||||
|
entityRanges: [{ key: 0, offset: 0, length: 1 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entityMap: {
|
||||||
|
"0": {
|
||||||
|
key: "0",
|
||||||
|
value: {
|
||||||
|
type: "MEDIA",
|
||||||
|
mutability: "Immutable",
|
||||||
|
data: {
|
||||||
|
caption: "Demo reel",
|
||||||
|
mediaItems: [{ mediaId: "vid-1" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
media_entities: [
|
||||||
|
{
|
||||||
|
media_id: "vid-1",
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 256000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
expect(markdown).toContain("Intro text.");
|
||||||
|
expect(markdown).toContain(``);
|
||||||
|
expect(markdown).toContain(`[video](${videoUrl})`);
|
||||||
|
expect(markdown).not.toContain(``);
|
||||||
|
expect(markdown).not.toContain("## Media");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders unused article videos in trailing media section", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/456/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/456/vid/avc1/1080x1080/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Trailing Media Example",
|
||||||
|
plain_text: "Body text.",
|
||||||
|
media_entities: [
|
||||||
|
{
|
||||||
|
media_id: "vid-2",
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 832000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown, coverUrl } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
expect(coverUrl).toBeNull();
|
||||||
|
expect(markdown).toContain("## Media");
|
||||||
|
expect(markdown).toContain(``);
|
||||||
|
expect(markdown).toContain(`[video](${videoUrl})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown keeps coverUrl as preview image for video cover media", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/789/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/789/vid/avc1/720x720/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Video Cover Example",
|
||||||
|
plain_text: "Body text.",
|
||||||
|
cover_media: {
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 1280000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { coverUrl } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
expect(coverUrl).toBe(posterUrl);
|
||||||
|
});
|
||||||
@@ -18,6 +18,17 @@ export type FormatArticleOptions = {
|
|||||||
referencedTweets?: Map<string, ReferencedTweetInfo>;
|
referencedTweets?: Map<string, ReferencedTweetInfo>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ResolvedMediaAsset =
|
||||||
|
| {
|
||||||
|
kind: "image";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "video";
|
||||||
|
url: string;
|
||||||
|
posterUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function coerceArticleEntity(value: unknown): ArticleEntity | null {
|
function coerceArticleEntity(value: unknown): ArticleEntity | null {
|
||||||
if (!value || typeof value !== "object") return null;
|
if (!value || typeof value !== "object") return null;
|
||||||
const candidate = value as ArticleEntity;
|
const candidate = value as ArticleEntity;
|
||||||
@@ -109,58 +120,127 @@ function resolveEntityEntry(
|
|||||||
return entityMap[String(entityKey)];
|
return entityMap[String(entityKey)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMediaUrl(info?: ArticleMediaInfo): string | undefined {
|
function resolveVideoUrl(info?: ArticleMediaInfo): string | undefined {
|
||||||
if (!info) return undefined;
|
if (!info) return undefined;
|
||||||
if (info.original_img_url) return info.original_img_url;
|
|
||||||
if (info.preview_image?.original_img_url) return info.preview_image.original_img_url;
|
|
||||||
const variants = info.variants ?? [];
|
const variants = info.variants ?? [];
|
||||||
const mp4 = variants
|
const mp4 = variants
|
||||||
.filter((variant) => variant?.content_type?.includes("video"))
|
.filter((variant) => variant?.content_type?.includes("video"))
|
||||||
.sort((a, b) => (b.bit_rate ?? 0) - (a.bit_rate ?? 0))[0];
|
.sort((a, b) => (b.bit_rate ?? 0) - (a.bit_rate ?? 0))[0];
|
||||||
return mp4?.url ?? variants[0]?.url;
|
return mp4?.url ?? variants.find((variant) => typeof variant?.url === "string")?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMediaById(article: ArticleEntity): Map<string, string> {
|
function resolveMediaAsset(info?: ArticleMediaInfo): ResolvedMediaAsset | undefined {
|
||||||
const map = new Map<string, string>();
|
if (!info) return undefined;
|
||||||
|
|
||||||
|
const posterUrl = info.preview_image?.original_img_url ?? info.original_img_url;
|
||||||
|
const videoUrl = resolveVideoUrl(info);
|
||||||
|
if (videoUrl) {
|
||||||
|
return {
|
||||||
|
kind: "video",
|
||||||
|
url: videoUrl,
|
||||||
|
posterUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = info.original_img_url ?? info.preview_image?.original_img_url;
|
||||||
|
if (imageUrl) {
|
||||||
|
return {
|
||||||
|
kind: "image",
|
||||||
|
url: imageUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFallbackMediaAsset(rawUrl?: string): ResolvedMediaAsset | undefined {
|
||||||
|
if (!rawUrl) return undefined;
|
||||||
|
|
||||||
|
if (/^https:\/\/video\.twimg\.com\//i.test(rawUrl) || /\.(mp4|m4v|mov|webm)(?:$|[?#])/i.test(rawUrl)) {
|
||||||
|
return {
|
||||||
|
kind: "video",
|
||||||
|
url: rawUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "image",
|
||||||
|
url: rawUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCoverUrl(info?: ArticleMediaInfo): string | undefined {
|
||||||
|
if (!info) return undefined;
|
||||||
|
return info.original_img_url ?? info.preview_image?.original_img_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMediaIdentity(asset: ResolvedMediaAsset): string {
|
||||||
|
return asset.kind === "video"
|
||||||
|
? `video:${asset.url}:${asset.posterUrl ?? ""}`
|
||||||
|
: `image:${asset.url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMediaLines(
|
||||||
|
asset: ResolvedMediaAsset,
|
||||||
|
altText: string,
|
||||||
|
usedUrls: Set<string>
|
||||||
|
): string[] {
|
||||||
|
if (asset.kind === "video") {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (asset.posterUrl && !usedUrls.has(asset.posterUrl)) {
|
||||||
|
usedUrls.add(asset.posterUrl);
|
||||||
|
lines.push(``);
|
||||||
|
}
|
||||||
|
if (!usedUrls.has(asset.url)) {
|
||||||
|
usedUrls.add(asset.url);
|
||||||
|
lines.push(`[video](${asset.url})`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedUrls.has(asset.url)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
usedUrls.add(asset.url);
|
||||||
|
return [``];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMediaById(article: ArticleEntity): Map<string, ResolvedMediaAsset> {
|
||||||
|
const map = new Map<string, ResolvedMediaAsset>();
|
||||||
for (const entity of article.media_entities ?? []) {
|
for (const entity of article.media_entities ?? []) {
|
||||||
if (!entity?.media_id) continue;
|
if (!entity?.media_id) continue;
|
||||||
const url = resolveMediaUrl(entity.media_info);
|
const asset = resolveMediaAsset(entity.media_info);
|
||||||
if (url) {
|
if (asset) {
|
||||||
map.set(entity.media_id, url);
|
map.set(entity.media_id, asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectMediaUrls(
|
function collectMediaAssets(article: ArticleEntity): ResolvedMediaAsset[] {
|
||||||
article: ArticleEntity,
|
const assets: ResolvedMediaAsset[] = [];
|
||||||
usedUrls: Set<string>,
|
const seen = new Set<string>();
|
||||||
excludeUrl?: string
|
const addAsset = (asset?: ResolvedMediaAsset) => {
|
||||||
): string[] {
|
if (!asset) return;
|
||||||
const urls: string[] = [];
|
const identity = buildMediaIdentity(asset);
|
||||||
const addUrl = (url?: string) => {
|
if (seen.has(identity)) return;
|
||||||
if (!url) return;
|
seen.add(identity);
|
||||||
if (excludeUrl && url === excludeUrl) {
|
assets.push(asset);
|
||||||
usedUrls.add(url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (usedUrls.has(url)) return;
|
|
||||||
usedUrls.add(url);
|
|
||||||
urls.push(url);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const entity of article.media_entities ?? []) {
|
for (const entity of article.media_entities ?? []) {
|
||||||
addUrl(resolveMediaUrl(entity?.media_info));
|
addAsset(resolveMediaAsset(entity?.media_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEntityMediaLines(
|
function resolveEntityMediaLines(
|
||||||
entityKey: number | undefined,
|
entityKey: number | undefined,
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined,
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
entityLookup: EntityLookup,
|
entityLookup: EntityLookup,
|
||||||
mediaById: Map<string, string>,
|
mediaById: Map<string, ResolvedMediaAsset>,
|
||||||
usedUrls: Set<string>
|
usedUrls: Set<string>
|
||||||
): string[] {
|
): string[] {
|
||||||
if (entityKey === undefined) return [];
|
if (entityKey === undefined) return [];
|
||||||
@@ -182,17 +262,16 @@ function resolveEntityMediaLines(
|
|||||||
: typeof item?.media_id === "string"
|
: typeof item?.media_id === "string"
|
||||||
? item.media_id
|
? item.media_id
|
||||||
: undefined;
|
: undefined;
|
||||||
const url = mediaId ? mediaById.get(mediaId) : undefined;
|
const asset = mediaId ? mediaById.get(mediaId) : undefined;
|
||||||
if (url && !usedUrls.has(url)) {
|
if (asset) {
|
||||||
usedUrls.add(url);
|
lines.push(...renderMediaLines(asset, altText, usedUrls));
|
||||||
lines.push(``);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackUrl = typeof value.data?.url === "string" ? value.data.url : undefined;
|
const fallbackUrl = typeof value.data?.url === "string" ? value.data.url : undefined;
|
||||||
if (fallbackUrl && !usedUrls.has(fallbackUrl)) {
|
const fallbackAsset = resolveFallbackMediaAsset(fallbackUrl);
|
||||||
usedUrls.add(fallbackUrl);
|
if (fallbackAsset) {
|
||||||
lines.push(``);
|
lines.push(...renderMediaLines(fallbackAsset, altText, usedUrls));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
@@ -237,6 +316,22 @@ function resolveEntityTweetLines(
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveEntityMarkdownLines(
|
||||||
|
entityKey: number | undefined,
|
||||||
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
|
entityLookup: EntityLookup
|
||||||
|
): string[] {
|
||||||
|
if (entityKey === undefined) return [];
|
||||||
|
const entry = resolveEntityEntry(entityKey, entityMap, entityLookup);
|
||||||
|
const value = entry?.value;
|
||||||
|
if (!value || value.type !== "MARKDOWN") return [];
|
||||||
|
|
||||||
|
const markdown = typeof value.data?.markdown === "string" ? value.data.markdown : "";
|
||||||
|
const normalized = markdown.replace(/\r\n/g, "\n").trimEnd();
|
||||||
|
if (!normalized) return [];
|
||||||
|
return normalized.split("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function buildMediaLinkMap(
|
function buildMediaLinkMap(
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined
|
entityMap: ArticleContentState["entityMap"] | undefined
|
||||||
): Map<number, string> {
|
): Map<number, string> {
|
||||||
@@ -330,7 +425,7 @@ function renderContentBlocks(
|
|||||||
blocks: ArticleBlock[],
|
blocks: ArticleBlock[],
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined,
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
entityLookup: EntityLookup,
|
entityLookup: EntityLookup,
|
||||||
mediaById: Map<string, string>,
|
mediaById: Map<string, ResolvedMediaAsset>,
|
||||||
usedUrls: Set<string>,
|
usedUrls: Set<string>,
|
||||||
mediaLinkMap: Map<number, string>,
|
mediaLinkMap: Map<number, string>,
|
||||||
referencedTweets?: Map<string, ReferencedTweetInfo>
|
referencedTweets?: Map<string, ReferencedTweetInfo>
|
||||||
@@ -397,6 +492,16 @@ function renderContentBlocks(
|
|||||||
return [...new Set(linkLines)];
|
return [...new Set(linkLines)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const collectMarkdownLines = (block: ArticleBlock): string[] => {
|
||||||
|
const ranges = Array.isArray(block.entityRanges) ? block.entityRanges : [];
|
||||||
|
const markdownLines: string[] = [];
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (typeof range?.key !== "number") continue;
|
||||||
|
markdownLines.push(...resolveEntityMarkdownLines(range.key, entityMap, entityLookup));
|
||||||
|
}
|
||||||
|
return markdownLines;
|
||||||
|
};
|
||||||
|
|
||||||
const pushTrailingMedia = (mediaLines: string[]) => {
|
const pushTrailingMedia = (mediaLines: string[]) => {
|
||||||
if (mediaLines.length > 0) {
|
if (mediaLines.length > 0) {
|
||||||
pushBlock(mediaLines, "media");
|
pushBlock(mediaLines, "media");
|
||||||
@@ -441,6 +546,11 @@ function renderContentBlocks(
|
|||||||
pushBlock(tweetLines, "quote");
|
pushBlock(tweetLines, "quote");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markdownLines = collectMarkdownLines(block);
|
||||||
|
if (markdownLines.length > 0) {
|
||||||
|
pushBlock(markdownLines, "text");
|
||||||
|
}
|
||||||
|
|
||||||
const mediaLines = collectMediaLines(block);
|
const mediaLines = collectMediaLines(block);
|
||||||
if (mediaLines.length > 0) {
|
if (mediaLines.length > 0) {
|
||||||
pushBlock(mediaLines, "media");
|
pushBlock(mediaLines, "media");
|
||||||
@@ -571,7 +681,7 @@ export function formatArticleMarkdown(
|
|||||||
lines.push(`# ${title}`);
|
lines.push(`# ${title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverUrl = resolveMediaUrl(candidate.cover_media?.media_info) ?? null;
|
const coverUrl = resolveCoverUrl(candidate.cover_media?.media_info) ?? null;
|
||||||
if (coverUrl) {
|
if (coverUrl) {
|
||||||
usedUrls.add(coverUrl);
|
usedUrls.add(coverUrl);
|
||||||
}
|
}
|
||||||
@@ -602,12 +712,13 @@ export function formatArticleMarkdown(
|
|||||||
lines.push(candidate.preview_text.trim());
|
lines.push(candidate.preview_text.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaUrls = collectMediaUrls(candidate, usedUrls, coverUrl ?? undefined);
|
const trailingMediaLines: string[] = [];
|
||||||
if (mediaUrls.length > 0) {
|
for (const asset of collectMediaAssets(candidate)) {
|
||||||
|
trailingMediaLines.push(...renderMediaLines(asset, "", usedUrls));
|
||||||
|
}
|
||||||
|
if (trailingMediaLines.length > 0) {
|
||||||
lines.push("", "## Media", "");
|
lines.push("", "## Media", "");
|
||||||
for (const url of mediaUrls) {
|
lines.push(...trailingMediaLines);
|
||||||
lines.push(``);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { markdown: lines.join("\n").trimEnd(), coverUrl };
|
return { markdown: lines.join("\n").trimEnd(), coverUrl };
|
||||||
|
|||||||
@@ -202,6 +202,13 @@ function toHighResUrl(rawUrl: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlausibleMediaUrl(rawUrl: string): boolean {
|
||||||
|
const ext = resolveExtensionFromUrl(rawUrl);
|
||||||
|
if (ext && (IMAGE_EXTENSIONS.has(ext) || VIDEO_EXTENSIONS.has(ext))) return true;
|
||||||
|
if (resolveKindFromHostname(rawUrl) !== undefined) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[] {
|
function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[] {
|
||||||
const candidates: MarkdownLinkCandidate[] = [];
|
const candidates: MarkdownLinkCandidate[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -221,10 +228,12 @@ function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[
|
|||||||
const label = match[1] ?? "";
|
const label = match[1] ?? "";
|
||||||
const rawUrl = match[3] ?? "";
|
const rawUrl = match[3] ?? "";
|
||||||
if (!rawUrl || seen.has(rawUrl)) continue;
|
if (!rawUrl || seen.has(rawUrl)) continue;
|
||||||
|
const isImage = label.startsWith("![");
|
||||||
|
if (!isImage && !isPlausibleMediaUrl(rawUrl)) continue;
|
||||||
seen.add(rawUrl);
|
seen.add(rawUrl);
|
||||||
candidates.push({
|
candidates.push({
|
||||||
url: rawUrl,
|
url: rawUrl,
|
||||||
hint: label.startsWith("![") ? "image" : "unknown",
|
hint: isImage ? "image" : "unknown",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type ArticleEntityMapEntry = {
|
|||||||
mutability?: string;
|
mutability?: string;
|
||||||
data?: {
|
data?: {
|
||||||
caption?: string;
|
caption?: string;
|
||||||
|
markdown?: string;
|
||||||
mediaItems?: ArticleEntityMapMediaItem[];
|
mediaItems?: ArticleEntityMapMediaItem[];
|
||||||
url?: string;
|
url?: string;
|
||||||
tweetId?: string;
|
tweetId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user