mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(agents): honor HTML bases and preserve adjacent imports
This commit is contained in:
1 parent
3dbd522d70
commit
464d829538
4 files changed
+86
-11
No files matched your search
@@ -61,6 +61,7 @@ URL attributes remove ASCII tabs/newlines throughout and discard surrounding
|
||||
ASCII control/space characters before resolution.
|
||||
Iframe/embed sources and object data attributes are document references and retain Markdown-target anchor checks.
|
||||
Inline iframe srcdoc documents are traversed too, with their own fragment anchors.
|
||||
The first active HTML base href sets reference resolution, including nested srcdoc bases.
|
||||
Explicit srcset attributes must contain at least one parsed candidate.
|
||||
Image and media references require nonempty targets that resolve to files, not directories.
|
||||
Image references check file existence without interpreting image fragments as
|
||||
@@ -69,7 +70,7 @@ HTML video, audio, source and track `src` assets and video posters use the same
|
||||
existence checks as images. Entity decoding uses full HTML text/attribute rules,
|
||||
including references whose semicolon may be omitted.
|
||||
Inline guidance imports are rejected after punctuation as well as whitespace.
|
||||
At-signs inside external URLs are excluded from the import scan.
|
||||
At-signs inside external URLs are excluded per HTML text node, preserving adjacent imports.
|
||||
Extensionless inline candidates are also imports when they resolve to repository files,
|
||||
checking the full filename before prefixes at ASCII/Unicode prose separators.
|
||||
Declared scoped dependencies, scope wildcards and matching TypeScript path aliases
|
||||
|
||||
@@ -59,9 +59,14 @@ function decodeEntities(text, attribute = false) {
|
||||
function htmlNavigation(html, inspect = () => {}) {
|
||||
const anchors = [];
|
||||
const references = [];
|
||||
let baseHref;
|
||||
function visit(node) {
|
||||
if (['script', 'style', 'template'].includes(node.tagName)) return;
|
||||
inspect(node);
|
||||
if (node.tagName === 'base' && baseHref === undefined)
|
||||
baseHref = node.attrs?.find(
|
||||
(attribute) => attribute.name === 'href'
|
||||
)?.value;
|
||||
for (const attribute of node.attrs ?? []) {
|
||||
if (
|
||||
attribute.name === 'id' ||
|
||||
@@ -97,7 +102,8 @@ function htmlNavigation(html, inspect = () => {}) {
|
||||
for (const reference of embedded.references)
|
||||
references.push(
|
||||
reference.target.startsWith('#') &&
|
||||
!reference.embeddedAnchors
|
||||
!reference.embeddedAnchors &&
|
||||
!reference.bases?.length
|
||||
? {
|
||||
...reference,
|
||||
embeddedAnchors: embedded.anchors,
|
||||
@@ -119,7 +125,16 @@ function htmlNavigation(html, inspect = () => {}) {
|
||||
for (const child of node.childNodes ?? []) visit(child);
|
||||
}
|
||||
visit(parseFragment(html));
|
||||
return { anchors, references };
|
||||
return {
|
||||
anchors,
|
||||
references:
|
||||
baseHref === undefined
|
||||
? references
|
||||
: references.map((reference) => ({
|
||||
...reference,
|
||||
bases: [baseHref, ...(reference.bases ?? [])],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function guidanceProse(markdown) {
|
||||
@@ -130,7 +145,11 @@ export function guidanceProse(markdown) {
|
||||
)
|
||||
)
|
||||
return ' ';
|
||||
if (node.nodeName === '#text') return node.value;
|
||||
if (node.nodeName === '#text')
|
||||
return node.value.replace(
|
||||
/(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]+/giu,
|
||||
' '
|
||||
);
|
||||
const content = (node.childNodes ?? []).map(text).join('');
|
||||
return [
|
||||
'address',
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
resolve,
|
||||
sep,
|
||||
} from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import {
|
||||
guidanceAnchors as anchors,
|
||||
guidanceProse,
|
||||
@@ -36,12 +36,24 @@ function within(root, path) {
|
||||
async function validateReference(
|
||||
rootDir,
|
||||
source,
|
||||
{ target, literal, image, unresolvedReference, embeddedAnchors }
|
||||
{ target, literal, image, unresolvedReference, embeddedAnchors, bases }
|
||||
) {
|
||||
if (unresolvedReference !== undefined)
|
||||
return `${source}: unresolved Markdown reference "${unresolvedReference}"`;
|
||||
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(target)) return;
|
||||
if (image && !target) return `${source}: empty media target`;
|
||||
if (bases?.length) {
|
||||
try {
|
||||
let base = pathToFileURL(resolve(rootDir, source));
|
||||
for (const href of bases) base = new URL(href, base);
|
||||
const url = new URL(target, base);
|
||||
if (url.protocol !== 'file:') return;
|
||||
target = url.pathname + url.search + url.hash;
|
||||
embeddedAnchors = undefined;
|
||||
} catch {
|
||||
return `${source}: malformed HTML base or target: ${target}`;
|
||||
}
|
||||
}
|
||||
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(target)) return;
|
||||
let path;
|
||||
let anchor;
|
||||
try {
|
||||
@@ -189,10 +201,7 @@ export async function validateAgentGuidance({ rootDir }) {
|
||||
diagnostics.push(
|
||||
`${source}: at most ${maxBytes} UTF-8 bytes allowed (received ${bytes})`
|
||||
);
|
||||
const prose = guidanceProse(markdown).replace(
|
||||
/(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]+/giu,
|
||||
' '
|
||||
);
|
||||
const prose = guidanceProse(markdown);
|
||||
const imports = guidanceStandaloneImports(markdown);
|
||||
const inlineImports = [];
|
||||
for (const match of prose.matchAll(
|
||||
|
||||
@@ -1432,3 +1432,49 @@ test('srcdoc anchors are scoped and templates inert', async (t) => {
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('autolink does not hide adjacent import', async (t) => {
|
||||
assert.ok(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'CLAUDE.md':
|
||||
'@AGENTS.md\n\n<https://example.com>).@docs/guide.md',
|
||||
})
|
||||
).some((message) => message.includes('additional or inline'))
|
||||
);
|
||||
});
|
||||
test('srcdoc base resolves local document paths', async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
"<iframe srcdoc=\"<base href='docs/'><a href='example.md#repeat'>Go</a>\"></iframe>",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
assert.ok(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
"<iframe srcdoc=\"<base href='missing/'><a href='docs/example.md'>Go</a>\"></iframe>",
|
||||
})
|
||||
).length > 0
|
||||
);
|
||||
});
|
||||
|
||||
test('srcdoc bases retain remote and repository containment rules', async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
"<iframe srcdoc=\"<base href='https://example.com/'><a href='guide.md'>Go</a>\"></iframe>",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
assert.ok(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
"<iframe srcdoc=\"<base href='../'><a href='guide.md'>Go</a>\"></iframe>",
|
||||
})
|
||||
).some((message) => message.includes('escapes repository'))
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user