Skip to content

Commit

Permalink
Merge pull request #169 from takker99:push-metadata
Browse files Browse the repository at this point in the history
feat(websocket): Push all page metadata
  • Loading branch information
takker99 committed May 2, 2024
2 parents 6aeb500 + 2a2590f commit b150280
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 151 deletions.
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

0 comments on commit b150280

Please sign in to comment.