Raymond Hill 2018-10-23 14:01:08 -03:00
parent 4a442eece4
commit cabb0d36b6
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
4 changed files with 194 additions and 275 deletions

View file

@ -140,8 +140,8 @@ var µBlock = (function() { // jshint ignore:line
// Read-only // Read-only
systemSettings: { systemSettings: {
compiledMagic: 5, // Increase when compiled format changes compiledMagic: 6, // Increase when compiled format changes
selfieMagic: 4 // Increase when selfie format changes selfieMagic: 6 // Increase when selfie format changes
}, },
restoreBackupSettings: { restoreBackupSettings: {

View file

@ -33,16 +33,15 @@
var µb = µBlock; var µb = µBlock;
// fedcba9876543210 // fedcba9876543210
// | | | ||| // | | |||
// | | | ||| // | | |||
// | | | ||| // | | |||
// | | | ||| // | | |||
// | | | ||+---- bit 0: [BlockAction | AllowAction] // | | ||+---- bit 0: [BlockAction | AllowAction]
// | | | |+----- bit 1: `important` // | | |+----- bit 1: `important`
// | | | +------ bit 2- 3: party [0 - 3] // | | +------ bit 2- 3: party [0 - 3]
// | | +-------- bit 4- 8: type [0 - 31] // | +-------- bit 4- 8: type [0 - 31]
// | +------------- bit 9-14: unused // +------------- bit 9-15: unused
// +------------------- bit 15: bad filter
var BlockAction = 0 << 0; var BlockAction = 0 << 0;
var AllowAction = 1 << 0; var AllowAction = 1 << 0;
@ -50,7 +49,6 @@ var Important = 1 << 1;
var AnyParty = 0 << 2; var AnyParty = 0 << 2;
var FirstParty = 1 << 2; var FirstParty = 1 << 2;
var ThirdParty = 2 << 2; var ThirdParty = 2 << 2;
var BadFilter = 1 << 15;
var AnyType = 0 << 4; var AnyType = 0 << 4;
var typeNameToTypeValue = { var typeNameToTypeValue = {
@ -126,50 +124,6 @@ var pageHostnameRegister = '',
//var filterRegister = null; //var filterRegister = null;
//var categoryRegister = ''; //var categoryRegister = '';
/******************************************************************************/
var histogram = function() {};
/*
histogram = function(label, categories) {
var h = [],
categoryBucket;
for ( var k in categories ) {
// No need for hasOwnProperty() here: there is no prototype chain.
categoryBucket = categories[k];
for ( var kk in categoryBucket ) {
// No need for hasOwnProperty() here: there is no prototype chain.
filterBucket = categoryBucket[kk];
h.push({
k: k.charCodeAt(0).toString(2) + ' ' + kk,
n: filterBucket instanceof FilterBucket ? filterBucket.filters.length : 1
});
}
}
console.log('Histogram %s', label);
var total = h.length;
h.sort(function(a, b) { return b.n - a.n; });
// Find indices of entries of interest
var target = 2;
for ( var i = 0; i < total; i++ ) {
if ( h[i].n === target ) {
console.log('\tEntries with only %d filter(s) start at index %s (key = "%s")', target, i, h[i].k);
target -= 1;
}
}
h = h.slice(0, 50);
h.forEach(function(v) {
console.log('\tkey=%s count=%d', v.k, v.n);
});
console.log('\tTotal buckets count: %d', total);
};
*/
/******************************************************************************/
// Local helpers // Local helpers
// Be sure to not confuse 'example.com' with 'anotherexample.com' // Be sure to not confuse 'example.com' with 'anotherexample.com'
@ -219,16 +173,20 @@ rawToRegexStr.escape4 = /\*/g;
rawToRegexStr.reTextHostnameAnchor1 = '^[a-z-]+://(?:[^/?#]+\\.)?'; rawToRegexStr.reTextHostnameAnchor1 = '^[a-z-]+://(?:[^/?#]+\\.)?';
rawToRegexStr.reTextHostnameAnchor2 = '^[a-z-]+://(?:[^/?#]+)?'; rawToRegexStr.reTextHostnameAnchor2 = '^[a-z-]+://(?:[^/?#]+)?';
var filterFingerprinter = µb.CompiledLineWriter.fingerprint; const filterDataSerialize = µb.CompiledLineIO.serialize;
var toLogDataInternal = function(categoryBits, tokenHash, filter) { var toLogDataInternal = function(categoryBits, tokenHash, filter) {
if ( filter === null ) { return undefined; } if ( filter === null ) { return undefined; }
var logData = filter.logData(); let logData = filter.logData();
logData.compiled = filterFingerprinter([ categoryBits, tokenHash, logData.compiled ]); logData.compiled = filterDataSerialize([
categoryBits,
tokenHash,
logData.compiled
]);
if ( categoryBits & 0x001 ) { if ( categoryBits & 0x001 ) {
logData.raw = '@@' + logData.raw; logData.raw = '@@' + logData.raw;
} }
var opts = []; let opts = [];
if ( categoryBits & 0x002 ) { if ( categoryBits & 0x002 ) {
opts.push('important'); opts.push('important');
} }
@ -237,7 +195,7 @@ var toLogDataInternal = function(categoryBits, tokenHash, filter) {
} else if ( categoryBits & 0x004 ) { } else if ( categoryBits & 0x004 ) {
opts.push('first-party'); opts.push('first-party');
} }
var type = categoryBits & 0x1F0; let type = categoryBits & 0x1F0;
if ( type !== 0 && type !== typeNameToTypeValue.data ) { if ( type !== 0 && type !== typeNameToTypeValue.data ) {
opts.push(typeValueToTypeName[type >>> 4]); opts.push(typeValueToTypeName[type >>> 4]);
} }
@ -300,16 +258,12 @@ var registerFilterClass = function(ctor) {
var fid = filterClassIdGenerator++; var fid = filterClassIdGenerator++;
ctor.fid = ctor.prototype.fid = fid; ctor.fid = ctor.prototype.fid = fid;
filterClasses[fid] = ctor; filterClasses[fid] = ctor;
//console.log(ctor.name, fid);
}; };
var filterFromCompiledData = function(args) { var filterFromCompiledData = function(args) {
//filterClassHistogram.set(fid, (filterClassHistogram.get(fid) || 0) + 1);
return filterClasses[args[0]].load(args); return filterClasses[args[0]].load(args);
}; };
//var filterClassHistogram = new Map();
/******************************************************************************/ /******************************************************************************/
var FilterTrue = function() { var FilterTrue = function() {
@ -1424,7 +1378,7 @@ FilterParser.prototype.toNormalizedType = {
FilterParser.prototype.reset = function() { FilterParser.prototype.reset = function() {
this.action = BlockAction; this.action = BlockAction;
this.anchor = 0; this.anchor = 0;
this.badFilter = 0; this.badFilter = false;
this.dataType = undefined; this.dataType = undefined;
this.dataStr = undefined; this.dataStr = undefined;
this.elemHiding = false; this.elemHiding = false;
@ -1594,7 +1548,7 @@ FilterParser.prototype.parseOptions = function(s) {
} }
// https://github.com/uBlockOrigin/uAssets/issues/192 // https://github.com/uBlockOrigin/uAssets/issues/192
if ( opt === 'badfilter' ) { if ( opt === 'badfilter' ) {
this.badFilter = BadFilter; this.badFilter = true;
continue; continue;
} }
// Unrecognized filter option: ignore whole filter. // Unrecognized filter option: ignore whole filter.
@ -2000,17 +1954,12 @@ FilterContainer.prototype.reset = function() {
this.allowFilterCount = 0; this.allowFilterCount = 0;
this.blockFilterCount = 0; this.blockFilterCount = 0;
this.discardedCount = 0; this.discardedCount = 0;
this.goodFilters = new Set();
this.badFilters = new Set(); this.badFilters = new Set();
this.duplicateBuster = new Set();
this.categories = new Map(); this.categories = new Map();
this.dataFilters = new Map(); this.dataFilters = new Map();
this.filterParser.reset(); this.filterParser.reset();
// Reuse filter instances whenever possible at load time.
this.fclassLast = null;
this.fdataLast = null;
this.filterLast = null;
// Runtime registers // Runtime registers
this.cbRegister = undefined; this.cbRegister = undefined;
this.thRegister = undefined; this.thRegister = undefined;
@ -2020,18 +1969,82 @@ FilterContainer.prototype.reset = function() {
/******************************************************************************/ /******************************************************************************/
FilterContainer.prototype.freeze = function() { FilterContainer.prototype.freeze = function() {
histogram('allFilters', this.categories); let filterPairId = FilterPair.fid,
this.removeBadFilters(); filterBucketId = FilterBucket.fid,
this.duplicateBuster = new Set(); filterDataHolderId = FilterDataHolder.fid,
redirectTypeValue = typeNameToTypeValue.redirect,
unserialize = µb.CompiledLineIO.unserialize;
for ( let line of this.goodFilters ) {
if ( this.badFilters.has(line) ) { continue; }
let args = unserialize(line);
let bits = args[0];
// Special cases: delegate to more specialized engines.
// Redirect engine.
if ( (bits & 0x1F0) === redirectTypeValue ) {
µb.redirectEngine.fromCompiledRule(args[1]);
continue;
}
// Plain static filters.
let tokenHash = args[1];
let fdata = args[2];
// Special treatment: data-holding filters are stored separately
// because they require special matching algorithm (unlike other
// filters, ALL hits must be reported).
if ( fdata[0] === filterDataHolderId ) {
let entry = new FilterDataHolderEntry(bits, tokenHash, fdata);
let bucket = this.dataFilters.get(tokenHash);
if ( bucket !== undefined ) {
entry.next = bucket;
}
this.dataFilters.set(tokenHash, entry);
continue;
}
let bucket = this.categories.get(bits);
if ( bucket === undefined ) {
bucket = new Map();
this.categories.set(bits, bucket);
}
let entry = bucket.get(tokenHash);
if ( tokenHash === this.dotTokenHash ) {
if ( entry === undefined ) {
entry = new FilterHostnameDict();
bucket.set(this.dotTokenHash, entry);
}
entry.add(fdata);
continue;
}
if ( entry === undefined ) {
bucket.set(tokenHash, filterFromCompiledData(fdata));
continue;
}
if ( entry.fid === filterBucketId ) {
entry.add(fdata);
continue;
}
if ( entry.fid === filterPairId ) {
bucket.set(
tokenHash,
entry.upgrade(filterFromCompiledData(fdata))
);
continue;
}
bucket.set(
tokenHash,
new FilterPair(entry, filterFromCompiledData(fdata))
);
}
this.filterParser.reset(); this.filterParser.reset();
this.fclassLast = null; this.goodFilters = new Set();
this.fdataLast = null;
this.filterLast = null;
this.frozen = true; this.frozen = true;
//console.log(JSON.stringify(Array.from(filterClassHistogram)));
//this.tokenHistogram = new Map(Array.from(this.tokenHistogram).sort(function(a, b) {
// return a[0].localeCompare(b[0]) || (b[1] - a[1]);
//}));
}; };
/******************************************************************************/ /******************************************************************************/
@ -2125,9 +2138,6 @@ FilterContainer.prototype.compile = function(raw, writer) {
return false; return false;
} }
// 0 = network filters
writer.select(0);
// Pure hostnames, use more efficient dictionary lookup // Pure hostnames, use more efficient dictionary lookup
// https://github.com/chrisaljoudi/uBlock/issues/665 // https://github.com/chrisaljoudi/uBlock/issues/665
// Create a dict keyed on request type etc. // Create a dict keyed on request type etc.
@ -2207,10 +2217,16 @@ FilterContainer.prototype.compileToAtomicFilter = function(
fdata, fdata,
writer writer
) { ) {
let descBits = parsed.action |
parsed.important | // 0 = network filters
parsed.party | // 1 = network filters: bad filters
parsed.badFilter; if ( parsed.badFilter ) {
writer.select(1);
} else {
writer.select(0);
}
let descBits = parsed.action | parsed.important | parsed.party;
let type = parsed.types; let type = parsed.types;
// Typeless // Typeless
@ -2231,7 +2247,7 @@ FilterContainer.prototype.compileToAtomicFilter = function(
// Only static filter with an explicit type can be redirected. If we reach // Only static filter with an explicit type can be redirected. If we reach
// this point, it's because there is one or more explicit type. // this point, it's because there is one or more explicit type.
if ( parsed.badFilter === 0 && parsed.redirect ) { if ( parsed.badFilter === false && parsed.redirect ) {
let redirects = µb.redirectEngine.compileRuleFromStaticFilter(parsed.raw); let redirects = µb.redirectEngine.compileRuleFromStaticFilter(parsed.raw);
if ( Array.isArray(redirects) ) { if ( Array.isArray(redirects) ) {
for ( let redirect of redirects ) { for ( let redirect of redirects ) {
@ -2244,138 +2260,30 @@ FilterContainer.prototype.compileToAtomicFilter = function(
/******************************************************************************/ /******************************************************************************/
FilterContainer.prototype.fromCompiledContent = function(reader) { FilterContainer.prototype.fromCompiledContent = function(reader) {
var badFilterBit = BadFilter,
filterPairId = FilterPair.fid,
filterBucketId = FilterBucket.fid,
filterDataHolderId = FilterDataHolder.fid,
redirectTypeValue = typeNameToTypeValue.redirect,
args, bits, bucket, entry,
tokenHash, fdata, fingerprint;
// 0 = network filters // 0 = network filters
reader.select(0); reader.select(0);
while ( reader.next() ) {
while ( reader.next() === true ) { if ( this.goodFilters.has(reader.line) ) {
args = reader.args();
bits = args[0];
if ( (bits & badFilterBit) !== 0 ) {
this.badFilters.add(args);
continue;
}
// Special cases: delegate to more specialized engines.
// Redirect engine.
if ( (bits & 0x1F0) === redirectTypeValue ) {
µb.redirectEngine.fromCompiledRule(args[1]);
continue;
}
this.acceptedCount += 1;
// Plain static filters.
fingerprint = reader.fingerprint();
tokenHash = args[1];
fdata = args[2];
// Special treatment: data-holding filters are stored separately
// because they require special matching algorithm (unlike other
// filters, ALL hits must be reported).
if ( fdata[0] === filterDataHolderId ) {
if ( this.duplicateBuster.has(fingerprint) ) {
this.discardedCount += 1;
continue;
}
this.duplicateBuster.add(fingerprint);
entry = new FilterDataHolderEntry(bits, tokenHash, fdata);
bucket = this.dataFilters.get(tokenHash);
if ( bucket !== undefined ) {
entry.next = bucket;
}
this.dataFilters.set(tokenHash, entry);
continue;
}
bucket = this.categories.get(bits);
if ( bucket === undefined ) {
bucket = new Map();
this.categories.set(bits, bucket);
}
entry = bucket.get(tokenHash);
if ( tokenHash === this.dotTokenHash ) {
if ( entry === undefined ) {
entry = new FilterHostnameDict();
bucket.set(this.dotTokenHash, entry);
}
if ( entry.add(fdata) === false ) {
this.discardedCount += 1;
}
continue;
}
if ( this.duplicateBuster.has(fingerprint) ) {
this.discardedCount += 1; this.discardedCount += 1;
continue; continue;
} }
this.duplicateBuster.add(fingerprint); this.goodFilters.add(reader.line);
this.acceptedCount += 1;
if ( entry === undefined ) {
bucket.set(tokenHash, filterFromCompiledData(fdata));
continue;
}
if ( entry.fid === filterBucketId ) {
entry.add(fdata);
continue;
}
if ( entry.fid === filterPairId ) {
bucket.set(
tokenHash,
entry.upgrade(filterFromCompiledData(fdata))
);
continue;
}
bucket.set(
tokenHash,
new FilterPair(entry, filterFromCompiledData(fdata))
);
} }
};
/******************************************************************************/ // 1 = network filters: bad filters
// Since we are going to keep bad filter fingerprints around, we ensure
FilterContainer.prototype.removeBadFilters = function() { // they are "detached" from the parent string from which they are sliced.
var filterPairId = FilterPair.fid, // We keep bad filter fingerprints around to use them when user
filterBucketId = FilterBucket.fid, // incrementally add filters (through "Block element" for example).
filterHostnameDictId = FilterHostnameDict.fid, reader.select(1);
bits, tokenHash, fdata, bucket, entry; while ( reader.next() ) {
for ( var args of this.badFilters ) { if ( this.badFilters.has(reader.line) ) {
bits = args[0] & ~BadFilter; this.discardedCount += 1;
bucket = this.categories.get(bits); continue;
if ( bucket === undefined ) { continue; }
tokenHash = args[1];
entry = bucket.get(tokenHash);
if ( entry === undefined ) { continue; }
fdata = args[2];
if ( entry.fid === filterPairId || entry.fid === filterBucketId ) {
entry.remove(fdata);
entry = entry.downgrade();
if ( entry !== undefined ) {
bucket.set(tokenHash, entry);
} else {
bucket.delete(tokenHash);
}
} else if ( entry.fid === filterHostnameDictId ) {
entry.remove(fdata);
if ( entry.size === 0 ) {
bucket.delete(tokenHash);
}
} else if ( arrayStrictEquals(entry.compile(), fdata) ) {
bucket.delete(tokenHash);
}
if ( bucket.size === 0 ) {
this.categories.delete(bits);
} }
this.badFilters.add(µb.orphanizeString(reader.line));
this.acceptedCount += 1;
} }
}; };

View file

@ -743,27 +743,26 @@
// Lower minimum update period to 1 day. // Lower minimum update period to 1 day.
µBlock.extractFilterListMetadata = function(assetKey, raw) { µBlock.extractFilterListMetadata = function(assetKey, raw) {
var listEntry = this.availableFilterLists[assetKey]; let listEntry = this.availableFilterLists[assetKey];
if ( listEntry === undefined ) { return; } if ( listEntry === undefined ) { return; }
// Metadata expected to be found at the top of content. // Metadata expected to be found at the top of content.
var head = raw.slice(0, 1024), let head = raw.slice(0, 1024);
matches, v;
// https://github.com/gorhill/uBlock/issues/313 // https://github.com/gorhill/uBlock/issues/313
// Always try to fetch the name if this is an external filter list. // Always try to fetch the name if this is an external filter list.
if ( listEntry.title === '' || listEntry.group === 'custom' ) { if ( listEntry.title === '' || listEntry.group === 'custom' ) {
matches = head.match(/(?:^|\n)(?:!|# )[\t ]*Title[\t ]*:([^\n]+)/i); let matches = head.match(/(?:^|\n)(?:!|# )[\t ]*Title[\t ]*:([^\n]+)/i);
if ( matches !== null ) { if ( matches !== null ) {
// https://bugs.chromium.org/p/v8/issues/detail?id=2869 // https://bugs.chromium.org/p/v8/issues/detail?id=2869
// JSON.stringify/JSON.parse is to work around String.slice() // orphanizeString is to work around String.slice()
// potentially causing the whole raw filter list to be held in // potentially causing the whole raw filter list to be held in
// memory just because we cut out the title as a substring. // memory just because we cut out the title as a substring.
listEntry.title = JSON.parse(JSON.stringify(matches[1].trim())); listEntry.title = this.orphanizeString(matches[1].trim());
} }
} }
// Extract update frequency information // Extract update frequency information
matches = head.match(/(?:^|\n)(?:!|# )[\t ]*Expires[\t ]*:[\t ]*(\d+)[\t ]*(h)?/i); let matches = head.match(/(?:^|\n)(?:!|# )[\t ]*Expires[\t ]*:[\t ]*(\d+)[\t ]*(h)?/i);
if ( matches !== null ) { if ( matches !== null ) {
v = Math.max(parseInt(matches[1], 10), 1); let v = Math.max(parseInt(matches[1], 10), 1);
if ( matches[2] !== undefined ) { if ( matches[2] !== undefined ) {
v = Math.ceil(v / 24); v = Math.ceil(v / 24);
} }
@ -787,29 +786,28 @@
/******************************************************************************/ /******************************************************************************/
µBlock.compileFilters = function(rawText) { µBlock.compileFilters = function(rawText) {
var writer = new this.CompiledLineWriter(); let writer = new this.CompiledLineIO.Writer();
// Useful references: // Useful references:
// https://adblockplus.org/en/filter-cheatsheet // https://adblockplus.org/en/filter-cheatsheet
// https://adblockplus.org/en/filters // https://adblockplus.org/en/filters
var staticNetFilteringEngine = this.staticNetFilteringEngine, let staticNetFilteringEngine = this.staticNetFilteringEngine,
staticExtFilteringEngine = this.staticExtFilteringEngine, staticExtFilteringEngine = this.staticExtFilteringEngine,
reIsWhitespaceChar = /\s/, reIsWhitespaceChar = /\s/,
reMaybeLocalIp = /^[\d:f]/, reMaybeLocalIp = /^[\d:f]/,
reIsLocalhostRedirect = /\s+(?:0\.0\.0\.0|broadcasthost|localhost|local|ip6-\w+)\b/, reIsLocalhostRedirect = /\s+(?:0\.0\.0\.0|broadcasthost|localhost|local|ip6-\w+)\b/,
reLocalIp = /^(?:0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)/, reLocalIp = /^(?:0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)/,
line, c, pos,
lineIter = new this.LineIterator(this.processDirectives(rawText)); lineIter = new this.LineIterator(this.processDirectives(rawText));
while ( lineIter.eot() === false ) { while ( lineIter.eot() === false ) {
// rhill 2014-04-18: The trim is important here, as without it there // rhill 2014-04-18: The trim is important here, as without it there
// could be a lingering `\r` which would cause problems in the // could be a lingering `\r` which would cause problems in the
// following parsing code. // following parsing code.
line = lineIter.next().trim(); let line = lineIter.next().trim();
if ( line.length === 0 ) { continue; } if ( line.length === 0 ) { continue; }
// Strip comments // Strip comments
c = line.charAt(0); let c = line.charAt(0);
if ( c === '!' || c === '[' ) { continue; } if ( c === '!' || c === '[' ) { continue; }
// Parse or skip cosmetic filters // Parse or skip cosmetic filters
@ -828,7 +826,7 @@
// Don't remove: // Don't remove:
// ...#blah blah blah // ...#blah blah blah
// because some ABP filters uses the `#` character (URL fragment) // because some ABP filters uses the `#` character (URL fragment)
pos = line.indexOf('#'); let pos = line.indexOf('#');
if ( pos !== -1 && reIsWhitespaceChar.test(line.charAt(pos - 1)) ) { if ( pos !== -1 && reIsWhitespaceChar.test(line.charAt(pos - 1)) ) {
line = line.slice(0, pos).trim(); line = line.slice(0, pos).trim();
} }
@ -860,7 +858,7 @@
µBlock.applyCompiledFilters = function(rawText, firstparty) { µBlock.applyCompiledFilters = function(rawText, firstparty) {
if ( rawText === '' ) { return; } if ( rawText === '' ) { return; }
var reader = new this.CompiledLineReader(rawText); let reader = new this.CompiledLineIO.Reader(rawText);
this.staticNetFilteringEngine.fromCompiledContent(reader); this.staticNetFilteringEngine.fromCompiledContent(reader);
this.staticExtFilteringEngine.fromCompiledContent(reader, { this.staticExtFilteringEngine.fromCompiledContent(reader, {
skipGenericCosmetic: this.userSettings.ignoreGenericCosmeticFilters, skipGenericCosmetic: this.userSettings.ignoreGenericCosmeticFilters,

View file

@ -224,18 +224,47 @@
/******************************************************************************/ /******************************************************************************/
µBlock.CompiledLineWriter = function() { µBlock.CompiledLineIO = {
this.blockId = undefined; serialize: JSON.stringify,
this.block = undefined; unserialize: JSON.parse,
this.blocks = new Map(); blockStartPrefix: '#block-start-', // ensure no special regex characters
this.stringifier = JSON.stringify; blockEndPrefix: '#block-end-', // ensure no special regex characters
Writer: function() {
this.io = µBlock.CompiledLineIO;
this.blockId = undefined;
this.block = undefined;
this.blocks = new Map();
this.stringifier = this.io.serialize;
},
Reader: function(raw, blockId) {
this.io = µBlock.CompiledLineIO;
this.block = '';
this.len = 0;
this.offset = 0;
this.line = '';
this.parser = this.io.unserialize;
this.blocks = new Map();
let reBlockStart = new RegExp(
'^' + this.io.blockStartPrefix + '(\\d+)\\n',
'gm'
);
let match = reBlockStart.exec(raw);
while ( match !== null ) {
let beg = match.index + match[0].length;
let end = raw.indexOf(this.io.blockEndPrefix + match[1], beg);
this.blocks.set(parseInt(match[1], 10), raw.slice(beg, end));
reBlockStart.lastIndex = end;
match = reBlockStart.exec(raw);
}
if ( blockId !== undefined ) {
this.select(blockId);
}
}
}; };
µBlock.CompiledLineWriter.fingerprint = function(args) { µBlock.CompiledLineIO.Writer.prototype = {
return JSON.stringify(args);
};
µBlock.CompiledLineWriter.prototype = {
push: function(args) { push: function(args) {
this.block[this.block.length] = this.stringifier(args); this.block[this.block.length] = this.stringifier(args);
}, },
@ -248,50 +277,26 @@
} }
}, },
toString: function() { toString: function() {
var result = []; let result = [];
for ( var entry of this.blocks ) { for ( let [ id, lines ] of this.blocks ) {
if ( entry[1].length === 0 ) { continue; } if ( lines.length === 0 ) { continue; }
result.push( result.push(
'#block-start-' + entry[0], this.io.blockStartPrefix + id,
entry[1].join('\n'), lines.join('\n'),
'#block-end-' + entry[0] this.io.blockEndPrefix + id
); );
} }
return result.join('\n'); return result.join('\n');
} }
}; };
/******************************************************************************/ µBlock.CompiledLineIO.Reader.prototype = {
µBlock.CompiledLineReader = function(raw, blockId) {
this.block = '';
this.len = 0;
this.offset = 0;
this.line = '';
this.parser = JSON.parse;
this.blocks = new Map();
var reBlockStart = /^#block-start-(\d+)\n/gm,
match = reBlockStart.exec(raw),
beg, end;
while ( match !== null ) {
beg = match.index + match[0].length;
end = raw.indexOf('#block-end-' + match[1], beg);
this.blocks.set(parseInt(match[1], 10), raw.slice(beg, end));
reBlockStart.lastIndex = end;
match = reBlockStart.exec(raw);
}
if ( blockId !== undefined ) {
this.select(blockId);
}
};
µBlock.CompiledLineReader.prototype = {
next: function() { next: function() {
if ( this.offset === this.len ) { if ( this.offset === this.len ) {
this.line = ''; this.line = '';
return false; return false;
} }
var pos = this.block.indexOf('\n', this.offset); let pos = this.block.indexOf('\n', this.offset);
if ( pos !== -1 ) { if ( pos !== -1 ) {
this.line = this.block.slice(this.offset, pos); this.line = this.block.slice(this.offset, pos);
this.offset = pos + 1; this.offset = pos + 1;
@ -466,3 +471,11 @@
return decomposed; return decomposed;
}; };
})(); })();
/******************************************************************************/
// TODO: evaluate using TextEncoder/TextDecoder
µBlock.orphanizeString = function(s) {
return JSON.parse(JSON.stringify(s));
};