ework-web 0.10.94 → 0.10.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/render/markdown.ts +31 -2
- package/src/views/issueThread.ts +9 -8
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1850,7 +1850,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1850
1850
|
login: c.author,
|
|
1851
1851
|
avatar: "",
|
|
1852
1852
|
created_at: c.created_at,
|
|
1853
|
-
body_html: renderMarkdown(c.body),
|
|
1853
|
+
body_html: renderMarkdown(c.body, "", `/${owner}/${repo}/issues/${issue.number}`),
|
|
1854
1854
|
};
|
|
1855
1855
|
}
|
|
1856
1856
|
let closed = false;
|
package/src/render/markdown.ts
CHANGED
|
@@ -33,11 +33,40 @@ const PURIFY_OPTS = {
|
|
|
33
33
|
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|\/|\.\/|\.\.\/|#)/i,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
export function renderMarkdown(body: string, baseDir = ""): string {
|
|
36
|
+
export function renderMarkdown(body: string, baseDir = "", baseIssuePath = ""): string {
|
|
37
37
|
const dirty = marked.parse(body ?? "");
|
|
38
38
|
const html = typeof dirty === "string" ? dirty : "";
|
|
39
39
|
const clean = purify.sanitize(html, PURIFY_OPTS) as unknown as string;
|
|
40
|
-
return linkifyAbsPaths(linkifySessionIDs(clean), baseDir);
|
|
40
|
+
return linkifyIssueRefs(linkifyAbsPaths(linkifySessionIDs(clean), baseDir), baseIssuePath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Linkify #N issue references inside rendered issue content. Only applied
|
|
44
|
+
// when the caller supplies the current issue's path — same-repo shorthand
|
|
45
|
+
// (#204) resolves against it. Skips tag interiors and existing anchors,
|
|
46
|
+
// same token-scanning approach as linkifySessionIDs.
|
|
47
|
+
export function linkifyIssueRefs(html: string, baseIssuePath = ""): string {
|
|
48
|
+
if (!baseIssuePath) return html;
|
|
49
|
+
const issuePath = baseIssuePath.replace(/\/+$/, "");
|
|
50
|
+
let out = "";
|
|
51
|
+
let inA = false;
|
|
52
|
+
let inCode = false;
|
|
53
|
+
const re = /(<[^>]*>)|([^<]+)/g;
|
|
54
|
+
let m: RegExpExecArray | null;
|
|
55
|
+
while ((m = re.exec(html)) !== null) {
|
|
56
|
+
if (m[1] !== undefined) {
|
|
57
|
+
const tag = m[1];
|
|
58
|
+
if (/^<a[\s>]/i.test(tag)) inA = true;
|
|
59
|
+
else if (/^<\/a[\s>]/i.test(tag)) inA = false;
|
|
60
|
+
else if (/^<pre[\s>]/i.test(tag) || /^<code[\s>]/i.test(tag)) inCode = true;
|
|
61
|
+
else if (/^<\/pre[\s>]/i.test(tag) || /^<\/code[\s>]/i.test(tag)) inCode = false;
|
|
62
|
+
out += tag;
|
|
63
|
+
} else if (m[2] !== undefined) {
|
|
64
|
+
out += inA || inCode
|
|
65
|
+
? m[2]
|
|
66
|
+
: m[2].replace(/(^|[^\w#])#([1-9][0-9]{0,4})(?![0-9\w])/g, (_all, pre: string, num: string) => `${pre}<a href="${issuePath.replace(/\/issues\/\d+$/, "")}/issues/${num}">#${num}</a>`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
// Linkify ses_ IDs in text nodes only — skips tag interiors (don't corrupt
|
package/src/views/issueThread.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface IssueThreadPayload {
|
|
|
37
37
|
comments: CommentView[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function toView(c: CommentRow): CommentView {
|
|
40
|
+
function toView(c: CommentRow, issuePath = ""): CommentView {
|
|
41
41
|
return {
|
|
42
42
|
id: c.id,
|
|
43
43
|
tag: classifyActor(c.body, c.author_kind),
|
|
@@ -45,13 +45,13 @@ function toView(c: CommentRow): CommentView {
|
|
|
45
45
|
login: c.author,
|
|
46
46
|
avatar: "",
|
|
47
47
|
created_at: c.created_at,
|
|
48
|
-
body_html: renderMarkdown(c.body),
|
|
48
|
+
body_html: renderMarkdown(c.body, "", issuePath),
|
|
49
49
|
display_name: c.author_display_name ?? null,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export async function viewsFromComments(rows: CommentRow[]): Promise<CommentView[]> {
|
|
54
|
-
const views = rows.map((r) => toView(r));
|
|
53
|
+
export async function viewsFromComments(rows: CommentRow[], issuePath = ""): Promise<CommentView[]> {
|
|
54
|
+
const views = rows.map((r) => toView(r, issuePath));
|
|
55
55
|
await hydrateReactions(views);
|
|
56
56
|
return views;
|
|
57
57
|
}
|
|
@@ -105,12 +105,13 @@ export async function buildIssueThread(
|
|
|
105
105
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
106
106
|
const currentPage = totalPages;
|
|
107
107
|
const { rows } = await listCommentsPage(issue.id, currentPage, PAGE_SIZE);
|
|
108
|
-
const
|
|
108
|
+
const issuePath = `/${owner}/${repo}/issues/${issue.number}`;
|
|
109
|
+
const views = await viewsFromComments(rows, issuePath);
|
|
109
110
|
const hasOlder = currentPage > 1;
|
|
110
111
|
const displayViews = orderForDisplay(views, cfg.commentSort);
|
|
111
112
|
const payload = payloadFromComments(issue, displayViews, currentPage, hasOlder, cfg.commentSort);
|
|
112
113
|
|
|
113
|
-
const descriptionHtml = renderMarkdown(issue.body);
|
|
114
|
+
const descriptionHtml = renderMarkdown(issue.body, "", issuePath);
|
|
114
115
|
const descriptionCollapsed = issue.body.length > 1200;
|
|
115
116
|
const upstreamWebUrl = (() => {
|
|
116
117
|
const clone = getDefaultUpstreamUrl(project);
|
|
@@ -187,7 +188,7 @@ export async function fetchIssuePage(
|
|
|
187
188
|
const issue = await getIssueWithMeta(project.id, number);
|
|
188
189
|
if (!issue) throw new StoreError(404, `#${number} 不存在`);
|
|
189
190
|
const { rows, page: clamped } = await listCommentsPage(issue.id, page, PAGE_SIZE);
|
|
190
|
-
const views = await viewsFromComments(rows);
|
|
191
|
+
const views = await viewsFromComments(rows, `/${owner}/${repo}/issues/${issue.number}`);
|
|
191
192
|
return { issue, views, currentPage: clamped, hasOlder: clamped > 1 };
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -202,7 +203,7 @@ export async function fetchIssueSince(
|
|
|
202
203
|
const issue = await getIssueWithMeta(project.id, number);
|
|
203
204
|
if (!issue) throw new StoreError(404, `#${number} 不存在`);
|
|
204
205
|
const rows = await listCommentsSince(issue.id, sinceISO);
|
|
205
|
-
return await viewsFromComments(rows);
|
|
206
|
+
return await viewsFromComments(rows, `/${owner}/${repo}/issues/${issue.number}`);
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
export function safeJsonEmbed(v: unknown): string {
|