先核來源與已訂規則
本例舊 docs/record 的 ../files/a,物件搬到 target/assets/a.bin,新紀錄卻在 target/pages。照留原相對字樣,會錯指 target/files/a。內部參考須有已附物件對應,外部來源按此規則仍作外部。
| 紀錄 | 輸入 JSON |
|---|---|
| 原例輸入 | {"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]} |
比較兩條實際路徑
更正新參考 ../assets/a.bin,才解到已對應檔案。建立相對參考時,目標的查詢參數及片段也須照留。所有 example.invalid 網址只在本地解路徑,沒有發請求。
| 路徑 | 輸出 JSON |
|---|---|
| 錯誤路徑 | {"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"} |
| 更正路徑 | {"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"} |
import {DatabaseSync} from 'node:sqlite';
import {TextDecoder} from 'node:util';
import {inflateSync} from 'node:zlib';
import {posix} from 'node:path';
import {createHash} from 'node:crypto';
const rows=(db,sql)=>db.prepare(sql).all().map(r=>({...r}));
const failed=(action)=>{try{action();return null;}catch(e){return {code:e.errcode??e.code??e.name};}};
import assert from 'node:assert/strict';
const run=function run(i,corrected=true){
if(!corrected)return {accepted:true,reference:i.reference,resolved:new URL(i.reference,i.newBase).href};
const old=new URL(i.reference,i.oldBase);const oldBase=new URL(i.oldBase);
if(old.origin!==oldBase.origin)return {accepted:true,reference:i.reference,resolved:old.href};
const target=i.mapping.find(m=>m.source===old.href);
if(!target)return {accepted:false,error:'unmapped-internal-reference',original:i.reference,resolvedSource:old.href};
const dest=new URL(target.destination);const newBase=new URL(i.newBase);
const reference=dest.origin===newBase.origin?posix.relative(posix.dirname(newBase.pathname),dest.pathname)+dest.search+dest.hash:dest.href;
return {accepted:true,reference,resolved:new URL(reference,i.newBase).href};
};
const cases=[{"label":"misleading base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"},"corrected":false},{"label":"corrected base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"},"corrected":true},{"label":"external absolute reference retained","input":{"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"},"corrected":true},{"label":"unmapped internal object held","input":{"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"},"corrected":true},{"label":"mapped query and fragment retained","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]},"expected":{"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"},"corrected":true}];
for(const test of cases){
const actual=run(test.input,test.corrected);
assert.deepEqual(actual,test.expected,test.label);
console.log(test.label,actual);
}
下次載入前核邊界
外部絕對參考照留;內部未有對應,連已解舊目標一起暫停。已對應的 ?revision=2#part,須在新參考及解出的網址都保留。
| 情況 | 輸入 JSON | 預期輸出 JSON |
|---|---|---|
| 外部絕對參考 | {"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]} | {"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"} |
| 內部物件尚無對應 | {"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]} | {"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"} |
| 對應目標的查詢參數及片段 | {"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]} | {"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"} |
新紀錄可否直接沿用 ../files/a?
查看參考答案
不可,新基準會改它的目標。須用已對應的 ../assets/a.bin。