Skip to content

feat(websocket): Push all page metadata #169

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

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions browser/websocket/__snapshots__/findMetadata.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const snapshot = {};

snapshot[`findMetadata() 1`] = `
[
[
"ふつうの",
"リンク2",
"hashtag",
],
[
"/help-jp/外部リンク",
],
[
"scrapbox",
"takker",
],
"https://scrapbox.io/files/65f29c24974fd8002333b160.svg",
[
"65f29c24974fd8002333b160",
"65e7f82e03949c0024a367d0",
"65e7f4413bc95600258481fb",
],
[
"助けてhelpfeel!!",
],
[
"名前 [scrapbox.icon]",
"住所 [リンク2]を入れること",
"電話番号 #をつけてもリンクにならないよ",
"自分の強み 3個くらい列挙",
],
]
`;
35 changes: 35 additions & 0 deletions browser/websocket/findMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { findMetadata, getHelpfeels } from "./findMetadata.ts";
import { assertEquals, assertSnapshot } from "../../deps/testing.ts";

const text = `てすと
[ふつうの]リンク
 しかし\`これは[リンク]\`ではない

code:code
コードブロック中の[リンク]や画像[https://scrapbox.io/files/65f29c0c9045b5002522c8bb.svg]は無視される


? 助けてhelpfeel!!

table:infobox
名前 [scrapbox.icon]
住所 [リンク2]を入れること
電話番号 #をつけてもリンクにならないよ
自分の強み 3個くらい列挙

#hashtag もつけるといいぞ?
[/forum-jp]のようなリンクは対象外
[/help-jp/]もだめ
[/icons/なるほど.icon][takker.icon]
[/help-jp/外部リンク]

サムネを用意
[https://scrapbox.io/files/65f29c24974fd8002333b160.svg]

[https://scrapbox.io/files/65e7f4413bc95600258481fb.svg https://scrapbox.io/files/65e7f82e03949c0024a367d0.svg]`;

Deno.test("findMetadata()", (t) => assertSnapshot(t, findMetadata(text)));
Deno.test("getHelpfeels()", () =>
assertEquals(getHelpfeels(text.split("\n").map((text) => ({ text }))), [
"助けてhelpfeel!!",
]));
179 changes: 179 additions & 0 deletions browser/websocket/findMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { BaseLine, Node, parse } from "../../deps/scrapbox.ts";
import { toTitleLc } from "../../title.ts";
import { parseYoutube } from "../../parser/youtube.ts";

/** テキストに含まれているメタデータを取り出す
*
* @param text Scrapboxのテキスト
* @return 順に、links, projectLinks, icons, image, files, helpfeels, infoboxDefinition
*/
export const findMetadata = (
text: string,
): [
string[],
string[],
string[],
string | null,
string[],
string[],
string[],
] => {
const blocks = parse(text, { hasTitle: true }).flatMap((block) => {
switch (block.type) {
case "codeBlock":
case "title":
return [];
case "line":
case "table":
return block;
}
});

/** 重複判定用map
*
* bracket link とhashtagを区別できるようにしている
* - bracket linkならtrue
*
* linkの形状はbracket linkを優先している
*/
const linksLc = new Map<string, boolean>();
const links = [] as string[];
const projectLinksLc = new Set<string>();
const projectLinks = [] as string[];
const iconsLc = new Set<string>();
const icons = [] as string[];
let image: string | null = null;
const files = new Set<string>();
const helpfeels = new Set<string>();

const fileUrlPattern = new RegExp(
`${
location?.origin ?? "https://scrapbox.io"
}/files/([a-z0-9]{24})(?:|\\.[a-zA-Z0-9]+)(?:|\\?[^\\s]*)$`,
);

const lookup = (node: Node) => {
switch (node.type) {
case "hashTag":
if (linksLc.has(toTitleLc(node.href))) return;
linksLc.set(toTitleLc(node.href), false);
links.push(node.href);
return;
case "link":
switch (node.pathType) {
case "relative": {
const link = cutId(node.href);
if (linksLc.get(toTitleLc(link))) return;
linksLc.set(toTitleLc(link), true);
links.push(link);
return;
}
case "root": {
const link = cutId(node.href);
// ignore `/project` or `/project/`
if (/^\/[\w\d-]+\/?$/.test(link)) return;
if (projectLinksLc.has(toTitleLc(link))) return;
projectLinksLc.add(toTitleLc(link));
projectLinks.push(link);
return;
}
case "absolute": {
const props = parseYoutube(node.href);
if (props && props.pathType !== "list") {
image ??= `https://i.ytimg.com/vi/${props.videoId}/mqdefault.jpg`;
return;
}
const fileId = node.href.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
return;
}
default:
return;
}
case "icon":
case "strongIcon": {
if (node.pathType === "root") return;
if (iconsLc.has(toTitleLc(node.path))) return;
iconsLc.add(toTitleLc(node.path));
icons.push(node.path);
return;
}
case "image":
case "strongImage": {
image ??= node.src.endsWith("/thumb/1000")
? node.src.replace(/\/thumb\/1000$/, "/raw")
: node.src;
{
const fileId = node.src.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
}
if (node.type === "image") {
const fileId = node.link.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
}
return;
}
case "helpfeel":
helpfeels.add(node.text);
return;
case "numberList":
case "strong":
case "quote":
case "decoration": {
for (const n of node.nodes) {
lookup(n);
}
return;
}
default:
return;
}
};

const infoboxDefinition = [] as string[];

for (const block of blocks) {
switch (block.type) {
case "line":
for (const node of block.nodes) {
lookup(node);
}
continue;
case "table": {
for (const row of block.cells) {
for (const nodes of row) {
for (const node of nodes) {
lookup(node);
}
}
}
if (!["infobox", "cosense"].includes(block.fileName)) continue;
infoboxDefinition.push(
...block.cells.map((row) =>
row.map((cell) => cell.map((node) => node.raw).join("")).join("\t")
.trim()
),
);
continue;
}
}
}

return [
links,
projectLinks,
icons,
image,
[...files],
[...helpfeels],
infoboxDefinition,
];
};

const cutId = (link: string): string => link.replace(/#[a-f\d]{24,32}$/, "");

/** テキストからHelpfeel記法のentryだけ取り出す */
export const getHelpfeels = (lines: Pick<BaseLine, "text">[]): string[] =>
lines.flatMap(({ text }) =>
/^\s*\? .*$/.test(text) ? [text.trimStart().slice(2)] : []
);
10 changes: 10 additions & 0 deletions browser/websocket/isSameArray.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isSameArray } from "./isSameArray.ts";
import { assert } from "../../deps/testing.ts";

Deno.test("isSameArray()", () => {
assert(isSameArray([1, 2, 3], [1, 2, 3]));
assert(isSameArray([1, 2, 3], [3, 2, 1]));
assert(!isSameArray([1, 2, 3], [3, 2, 3]));
assert(!isSameArray([1, 2, 3], [1, 2]));
assert(isSameArray([], []));
});
2 changes: 2 additions & 0 deletions browser/websocket/isSameArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isSameArray = <T>(a: T[], b: T[]): boolean =>
a.length === b.length && a.every((x) => b.includes(x));
Loading
Loading