diff --git a/site-astro/src/notebook-page.html b/site-astro/src/notebook-page.html index ebf9118..26141b7 100644 --- a/site-astro/src/notebook-page.html +++ b/site-astro/src/notebook-page.html @@ -335,7 +335,7 @@ - + `, ``), hence `(?:\s[^>]*)?`. + const re = /]*)?>[\s\S]*?<\/script(?:\s[^>]*)?>/gi; let m: RegExpExecArray | null; while ((m = re.exec(content)) !== null) { const inner = m[0] .replace(/]*)?>/i, '') - .replace(/<\/script>/i, ''); + .replace(/<\/script(?:\s[^>]*)?>/i, ''); blocks.push(inner); } return blocks.length > 0 ? blocks.join('\n') : null; diff --git a/src/kb/view-template.ts b/src/kb/view-template.ts index 4ae565a..61d5138 100644 --- a/src/kb/view-template.ts +++ b/src/kb/view-template.ts @@ -32,4 +32,4 @@ * local file, often offline; a webfont token here would look correct in * the repo and in a screenshot and silently fail for the actual user. */ -export const VIEW_TEMPLATE ="__TITLE__\n\n\n
\n
\n \n
\n Coldstart Notebook\n repo\n
\n
\n
0notes
\n
0links
\n
\n
\n
0%fresh
\n
\n
\n \n \n
\n
\n\n
\n \n\n
\n
Select a note to read.
\n
\n\n
\n \n
\n
Note graph
\n
file note
\n
flow note
\n
Only linked notes shown. Click a node to open it.
\n
\n
\n
\n
\n
\n\n\n\n"; +export const VIEW_TEMPLATE ="__TITLE__\n\n\n
\n
\n \n
\n Coldstart Notebook\n repo\n
\n
\n
0notes
\n
0links
\n
\n
\n
0%fresh
\n
\n
\n \n \n
\n
\n\n
\n \n\n
\n
Select a note to read.
\n
\n\n
\n \n
\n
Note graph
\n
file note
\n
flow note
\n
Only linked notes shown. Click a node to open it.
\n
\n
\n
\n
\n
\n\n\n\n"; diff --git a/tests/java-ruby.test.ts b/tests/java-ruby.test.ts index bba488d..dcb4eb6 100644 --- a/tests/java-ruby.test.ts +++ b/tests/java-ruby.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFile } from '../src/indexer/parser.js'; -import { parseJavaContent } from '../src/indexer/extractors/java.js'; +import { parseJavaContent, stripGenerics } from '../src/indexer/extractors/java.js'; import { parseRubyContent } from '../src/indexer/extractors/ruby.js'; import { parseKotlinContent } from '../src/indexer/extractors/kotlin.js'; import { ensureParsersReady } from '../src/indexer/extractors/parser-factory.js'; @@ -113,6 +113,27 @@ describe('java-parser — symbol extraction (direct)', () => { expect(cls!.extendsName).toBe('AuthService'); }); + // CodeQL-driven fix: one `<[^>]*>` pass turned `Map>` into + // `Map>` — a name that matches no class, so the edge was silently lost. + it('stripGenerics removes NESTED generic arguments', () => { + expect(stripGenerics('List')).toBe('List'); + expect(stripGenerics('Map>')).toBe('Map'); + expect(stripGenerics('Foo>>')).toBe('Foo'); + expect(stripGenerics('Outer.Inner')).toBe('Outer.Inner'); // qualified names stay qualified + expect(stripGenerics('Plain')).toBe('Plain'); + }); + + it('extends / implements names survive nested generics', () => { + const src = ` + public class Repo extends Base>> implements Comparable, Handler> { + public void run() {} + } + `; + const cls = parseJavaContent(src, AUTH_FILE_ID).symbols.find(s => s.name === 'Repo'); + expect(cls!.extendsName).toBe('Base'); + expect(cls!.implementsNames).toEqual(['Comparable', 'Handler']); + }); + it('extracts method symbols from class body', () => { const src = ` public class AuthService { diff --git a/tests/kb-view.test.ts b/tests/kb-view.test.ts index 22341af..326e128 100644 --- a/tests/kb-view.test.ts +++ b/tests/kb-view.test.ts @@ -87,6 +87,23 @@ describe('kb view — buildViewData', () => { }); }); +describe('kb view — esc (attribute-safe)', () => { + // The template's `esc` feeds double-quoted attributes (data-dir="${esc(path)}"). + // Escaping only & < > let a path or title containing `"` break out of the + // attribute (CodeQL js/incomplete-html-attribute-sanitization). + const esc = new Function(`${VIEW_TEMPLATE.match(/const esc = [^\n]*;/)![0]}; return esc;`)() as (s: unknown) => string; + + it('escapes quotes so a value cannot close an attribute', () => { + const out = esc(`a" onmouseover="x' y`); + expect(out).not.toMatch(/["']/); + expect(out).toBe('a" onmouseover="x' y'); + }); + + it('still escapes the text-node characters', () => { + expect(esc('&')).toBe('<b>&'); + }); +}); + describe('kb view — renderViewHtml', () => { it('injects data and neutralizes so note text cannot break out', () => { const html = renderViewHtml(VIEW_TEMPLATE, { summary: { total: 1 }, notes: [{ body: 'evil text' }] }); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 60e5841..c607ee2 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { join, dirname } from 'node:path'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { parseFile } from '../src/indexer/parser.js'; import { parseTsContent } from '../src/indexer/ts-parser.js'; @@ -1270,3 +1272,19 @@ describe('Groovy / Gradle / Jenkinsfile extractor', () => { expect(result.exports).not.toContain('no-colons'); }); }); + +describe('parser — Vue SFC script extraction', () => { + // HTML parsers accept whitespace and junk attributes in an end tag; a script + // block closed that way must not swallow the rest of the file. + it.each(['', '', ''])('ends a script block at %j', async (close) => { + const dir = mkdtempSync(join(tmpdir(), 'coldstart-sfc-')); + try { + const f = join(dir, 'A.vue'); + writeFileSync(f, `\n\n`); + const r = await parseFile(f, 'vue'); + expect(r!.exports).toEqual(expect.arrayContaining(['first', 'second'])); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +});