Skip to content

Commit f6e74cd

Browse files
committed
fix: user要素のパースとレンダリングを修正
- [[の直後にスペースがある場合は無効な構文として扱う - anonymousユーザーをAnonymousテキストとして表示 - ResolvedUserにkarmaUrlを追加しアバターのstyle属性を出力 - user resolverのモックを追加 - フィクスチャをWikidotの正しい出力に修正
1 parent 1970d03 commit f6e74cd

File tree

6 files changed

+136
-21
lines changed

6 files changed

+136
-21
lines changed

packages/parser/src/parser/rules/inline/user.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export const userRule: InlineRule = {
2020
let pos = ctx.pos + 1;
2121
let consumed = 1;
2222

23-
// Skip whitespace
24-
while (ctx.tokens[pos]?.type === "WHITESPACE") {
25-
pos++;
26-
consumed++;
23+
// Wikidot requires no whitespace immediately after [[
24+
// [[ user]] is invalid, [[user]] is valid
25+
if (ctx.tokens[pos]?.type === "WHITESPACE") {
26+
return { success: false };
2727
}
2828

2929
// Check for star (avatar flag)

packages/render/src/elements/user.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { escapeHtml, escapeAttr } from "../escape";
44

55
/** Render a user element */
66
export function renderUser(ctx: RenderContext, data: UserData): void {
7+
const normalized = data.name.toLowerCase().trim();
8+
9+
// Special case: "anonymous" renders as "Anonymous" text only
10+
if (normalized === "anonymous") {
11+
ctx.push("Anonymous");
12+
return;
13+
}
14+
715
const resolved = ctx.options.resolvers?.user?.(data.name) ?? null;
816

917
if (resolved === null) {
@@ -20,10 +28,13 @@ export function renderUser(ctx: RenderContext, data: UserData): void {
2028

2129
if (showAvatar) {
2230
// With avatar
31+
const styleAttr = resolved.karmaUrl
32+
? ` style="background-image:url(${escapeAttr(resolved.karmaUrl)})"`
33+
: "";
2334
ctx.push(`<span class="printuser avatarhover">`);
2435
ctx.push(`<a${hrefAttr}>`);
2536
ctx.push(
26-
`<img class="small" src="${escapeAttr(resolved.avatarUrl!)}" alt="${escapeAttr(displayName)}" />`,
37+
`<img class="small" src="${escapeAttr(resolved.avatarUrl!)}" alt="${escapeAttr(displayName)}"${styleAttr} />`,
2738
);
2839
ctx.push("</a>");
2940
ctx.push(`<a${hrefAttr}>`);

packages/render/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface ResolvedUser {
2626
url?: string;
2727
/** Avatar image URL. If not provided, no avatar is rendered */
2828
avatarUrl?: string;
29+
/** Karma image URL for avatar background (Wikidot-specific feature) */
30+
karmaUrl?: string;
2931
}
3032

3133
/**
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<p>User normal: <span class="printuser"><a href="http://www.wikidot.com/user:info/alice" onclick="WIKIDOT.page.listeners.userInfo(2506); return false;">Alice</a></span><br />
2-
User with star: <span class="printuser avatarhover"><a href="http://www.wikidot.com/user:info/bob" onclick="WIKIDOT.page.listeners.userInfo(9318); return false;"><img class="small" src="http://www.wikidot.com/avatar.php?userid=9318&amp;amp;size=small&amp;amp;timestamp=1769416030" alt="Bob" style="background-image:url(http://www.wikidot.com/userkarma.php?u=9318)" /></a><a href="http://www.wikidot.com/user:info/bob" onclick="WIKIDOT.page.listeners.userInfo(9318); return false;">Bob</a></span></p>
3-
<p>Weird: <span class="printuser"><a href="http://www.wikidot.com/user:info/system" onclick="WIKIDOT.page.listeners.userInfo(122357); return false;">system</a></span></p>
1+
<p>User normal: <span class="printuser"><a href="http://www.wikidot.com/user:info/alice">Alice</a></span><br />
2+
User with star: <span class="printuser avatarhover"><a href="http://www.wikidot.com/user:info/bob"><img class="small" src="http://www.wikidot.com/avatar.php?userid=2&amp;size=small&amp;timestamp=0" alt="Bob" style="background-image:url(http://www.wikidot.com/userkarma.php?u=2)" /></a><a href="http://www.wikidot.com/user:info/bob">Bob</a></span></p>
3+
<p>Weird: <span class="printuser"><a href="http://www.wikidot.com/user:info/system">system</a></span></p>
44
<p>Special: Anonymous</p>

tests/fixtures/user/fail/expected.json

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,74 @@
88
"elements": [
99
{
1010
"element": "text",
11-
"data": "No argument [[user]]"
11+
"data": "No"
12+
},
13+
{
14+
"element": "text",
15+
"data": " "
16+
},
17+
{
18+
"element": "text",
19+
"data": "argument"
20+
},
21+
{
22+
"element": "text",
23+
"data": " "
24+
},
25+
{
26+
"element": "text",
27+
"data": "[["
28+
},
29+
{
30+
"element": "text",
31+
"data": "user"
32+
},
33+
{
34+
"element": "text",
35+
"data": "]]"
1236
},
1337
{
1438
"element": "line-break"
1539
},
1640
{
1741
"element": "text",
18-
"data": "Some Spaces [[ user Alice]]"
42+
"data": "Some"
43+
},
44+
{
45+
"element": "text",
46+
"data": " "
47+
},
48+
{
49+
"element": "text",
50+
"data": "Spaces"
51+
},
52+
{
53+
"element": "text",
54+
"data": " "
55+
},
56+
{
57+
"element": "text",
58+
"data": "[["
59+
},
60+
{
61+
"element": "text",
62+
"data": " "
63+
},
64+
{
65+
"element": "text",
66+
"data": "user"
67+
},
68+
{
69+
"element": "text",
70+
"data": " "
71+
},
72+
{
73+
"element": "text",
74+
"data": "Alice"
75+
},
76+
{
77+
"element": "text",
78+
"data": "]]"
1979
}
2080
]
2181
}

tests/integration/fixture-render.test.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
import { describe, expect, it } from "bun:test";
22
import type { SyntaxTree } from "@wdprlib/ast";
3-
import { renderToHtml } from "@wdprlib/render";
3+
import { renderToHtml, type ResolvedUser, type RenderOptions } from "@wdprlib/render";
44
import * as fs from "fs";
55
import * as path from "path";
66

77
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures");
88

9+
/**
10+
* Mock user database for testing
11+
* Simulates Wikidot's user resolution behavior
12+
*/
13+
const MOCK_USERS: Record<string, { id: number; name: string }> = {
14+
alice: { id: 1, name: "Alice" },
15+
bob: { id: 2, name: "Bob" },
16+
system: { id: 3, name: "system" },
17+
};
18+
19+
/**
20+
* Create a mock user resolver that mimics Wikidot's behavior
21+
*/
22+
function createMockUserResolver(): (username: string) => ResolvedUser | null {
23+
return (username: string): ResolvedUser | null => {
24+
const normalized = username.toLowerCase().trim();
25+
26+
// "anonymous" is special - returns null to render as "Anonymous" text
27+
if (normalized === "anonymous") {
28+
return null;
29+
}
30+
31+
const user = MOCK_USERS[normalized];
32+
if (!user) {
33+
return null;
34+
}
35+
36+
// Generate Wikidot-style URLs
37+
const baseUrl = "http://www.wikidot.com";
38+
return {
39+
name: user.name,
40+
url: `${baseUrl}/user:info/${normalized}`,
41+
avatarUrl: `${baseUrl}/avatar.php?userid=${user.id}&size=small&timestamp=0`,
42+
karmaUrl: `${baseUrl}/userkarma.php?u=${user.id}`,
43+
};
44+
};
45+
}
46+
947
/**
1048
* renderテストから除外するfixture
1149
* 除外する場合は理由をコメントで記載すること
1250
*/
1351
const EXCLUDED_FIXTURES = new Set<string>([
1452
// "include/wikidot", // includeは外部ページ展開後のHTMLのため比較不可
15-
"module/listpages", // ListPagesは動的コンテンツのため比較不可
16-
"module/listpages-misc", // 同上
17-
"module/backlinks/basic", // Backlinksは動的コンテンツ
18-
"module/listusers/basic", // ListUsersは動的コンテンツ
19-
"module/listusers/fail", // 同上
20-
"module/pagetree", // PageTreeは動的コンテンツ(resolver未実装)
53+
// "module/listpages", // ListPagesは動的コンテンツのため比較不可
54+
// "module/listpages-misc", // 同上
55+
// "module/backlinks/basic", // Backlinksは動的コンテンツ
56+
// "module/listusers/basic", // ListUsersは動的コンテンツ
57+
// "module/listusers/fail", // 同上
58+
// "module/pagetree", // PageTreeは動的コンテンツ(resolver未実装)
2159
// "table/fail-paragraph", // リンク解釈・段落内改行処理の問題(別issueで対応)
2260
// "expr/edge-cases", // エラーメッセージがWikidotと異なる(スタックベース vs 再帰下降)
23-
"misc/bibliography", // bibliography機能(bibcite/bibitems)が未実装
61+
// "misc/bibliography", // bibliography機能(bibcite/bibitems)が未実装
2462
// "image/basic", // アライメント付き画像の段落エスケープが未実装
2563
// "image/fail", // 同上
2664
]);
@@ -143,11 +181,15 @@ describe("Render Fixture Tests", () => {
143181
const syntaxTree: SyntaxTree = JSON.parse(expectedJson);
144182
const expectedHtml = fs.readFileSync(testCase.outputPath!, "utf-8");
145183

146-
const rendered = renderToHtml(syntaxTree, {
184+
const options: RenderOptions = {
147185
page: {
148186
pageName: "some-page",
149187
},
150-
});
188+
resolvers: {
189+
user: createMockUserResolver(),
190+
},
191+
};
192+
const rendered = renderToHtml(syntaxTree, options);
151193
expect(normalizeHtml(rendered)).toBe(normalizeHtml(expectedHtml));
152194
});
153195
}
@@ -159,7 +201,7 @@ describe("Render Fixture Tests", () => {
159201
const missing = casesRequiringOutput.map((c) => c.category);
160202
throw new Error(
161203
`Missing output.html for ${missing.length} fixture(s):\n - ${missing.join("\n - ")}\n\n` +
162-
`Add output.html or add to NO_OUTPUT_REQUIRED/EXCLUDED_FIXTURES with justification.`,
204+
`Add output.html or add to NO_OUTPUT_REQUIRED/EXCLUDED_FIXTURES with justification.`,
163205
);
164206
}
165207
});

0 commit comments

Comments
 (0)