|
import { getLocator } from 'locate-character'; |
|
import { |
|
MappedCode, |
|
parse_attached_sourcemap, |
|
sourcemap_add_offset, |
|
combine_sourcemaps |
|
} from '../utils/mapped_code.js'; |
|
import { decode_map } from './decode_sourcemap.js'; |
|
import { replace_in_code, slice_source } from './replace_in_code.js'; |
|
|
|
const regex_filepath_separator = /[/\\]/; |
|
|
|
|
|
|
|
|
|
function get_file_basename(filename) { |
|
return filename.split(regex_filepath_separator).pop(); |
|
} |
|
|
|
|
|
|
|
|
|
class PreprocessResult { |
|
|
|
source; |
|
|
|
filename; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sourcemap_list = []; |
|
|
|
|
|
|
|
|
|
|
|
dependencies = []; |
|
|
|
|
|
|
|
|
|
file_basename = undefined; |
|
|
|
|
|
|
|
|
|
get_location = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(source, filename) { |
|
this.source = source; |
|
this.filename = filename; |
|
this.update_source({ string: source }); |
|
|
|
this.file_basename = filename == null ? null : get_file_basename(filename); |
|
} |
|
|
|
|
|
|
|
|
|
update_source({ string: source, map, dependencies }) { |
|
if (source != null) { |
|
this.source = source; |
|
this.get_location = getLocator(source); |
|
} |
|
if (map) { |
|
this.sourcemap_list.unshift(map); |
|
} |
|
if (dependencies) { |
|
this.dependencies.push(...dependencies); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
to_processed() { |
|
|
|
const map = combine_sourcemaps(this.file_basename, this.sourcemap_list); |
|
return { |
|
|
|
|
|
|
|
|
|
code: this.source, |
|
dependencies: [...new Set(this.dependencies)], |
|
map, |
|
toString: () => this.source |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function processed_content_to_code(processed, location, file_basename) { |
|
|
|
|
|
|
|
|
|
|
|
let decoded_map; |
|
if (processed.map) { |
|
decoded_map = decode_map(processed); |
|
|
|
if (decoded_map.sources) { |
|
|
|
const source_index = decoded_map.sources.indexOf(file_basename); |
|
if (source_index !== -1) { |
|
sourcemap_add_offset(decoded_map, location, source_index); |
|
} |
|
} |
|
} |
|
return MappedCode.from_processed(processed.code, decoded_map); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function processed_tag_to_code( |
|
processed, |
|
tag_name, |
|
original_attributes, |
|
generated_attributes, |
|
source |
|
) { |
|
const { file_basename, get_location } = source; |
|
|
|
|
|
|
|
|
|
|
|
const build_mapped_code = (code, offset) => |
|
MappedCode.from_source(slice_source(code, offset, source)); |
|
|
|
|
|
|
|
|
|
const original_tag_open = `<${tag_name}${original_attributes}>`; |
|
const tag_open = `<${tag_name}${generated_attributes}>`; |
|
|
|
let tag_open_code; |
|
|
|
if (original_tag_open.length !== tag_open.length) { |
|
|
|
|
|
const mappings = [ |
|
[ |
|
|
|
[0, 0, 0, 0], |
|
|
|
[`<${tag_name}`.length, 0, 0, `<${tag_name}`.length] |
|
] |
|
]; |
|
|
|
const line = tag_open.split('\n').length - 1; |
|
const column = tag_open.length - (line === 0 ? 0 : tag_open.lastIndexOf('\n')) - 1; |
|
|
|
while (mappings.length <= line) { |
|
|
|
mappings.push([[0, 0, 0, `<${tag_name}`.length]]); |
|
} |
|
|
|
|
|
mappings[line].push([ |
|
column, |
|
0, |
|
original_tag_open.split('\n').length - 1, |
|
original_tag_open.length - original_tag_open.lastIndexOf('\n') - 1 |
|
]); |
|
|
|
|
|
const map = { |
|
version: 3, |
|
names: [], |
|
sources: [file_basename], |
|
mappings |
|
}; |
|
sourcemap_add_offset(map, get_location(0), 0); |
|
tag_open_code = MappedCode.from_processed(tag_open, map); |
|
} else { |
|
tag_open_code = build_mapped_code(tag_open, 0); |
|
} |
|
|
|
const tag_close = `</${tag_name}>`; |
|
const tag_close_code = build_mapped_code( |
|
tag_close, |
|
original_tag_open.length + source.source.length |
|
); |
|
|
|
parse_attached_sourcemap(processed, tag_name); |
|
const content_code = processed_content_to_code( |
|
processed, |
|
get_location(original_tag_open.length), |
|
file_basename |
|
); |
|
|
|
return tag_open_code.concat(content_code).concat(tag_close_code); |
|
} |
|
|
|
const attribute_pattern = /([\w-$]+\b)(?:=(?:"([^"]*)"|'([^']*)'|(\S+)))?/g; |
|
|
|
|
|
|
|
|
|
function parse_tag_attributes(str) { |
|
|
|
const attrs = {}; |
|
|
|
|
|
let match; |
|
while ((match = attribute_pattern.exec(str)) !== null) { |
|
const name = match[1]; |
|
const value = match[2] || match[3] || match[4]; |
|
attrs[name] = !value || value; |
|
} |
|
|
|
return attrs; |
|
} |
|
|
|
|
|
|
|
|
|
function stringify_tag_attributes(attributes) { |
|
if (!attributes) return; |
|
|
|
let value = Object.entries(attributes) |
|
.map(([key, value]) => (value === true ? key : `${key}="${value}"`)) |
|
.join(' '); |
|
if (value) { |
|
value = ' ' + value; |
|
} |
|
return value; |
|
} |
|
|
|
const regex_style_tags = |
|
/<!--[^]*?-->|<style((?:\s+[^=>'"\/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"\/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g; |
|
const regex_script_tags = |
|
/<!--[^]*?-->|<script((?:\s+[^=>'"\/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"\/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function process_tag(tag_name, preprocessor, source) { |
|
const { filename, source: markup } = source; |
|
const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags; |
|
|
|
|
|
|
|
|
|
const dependencies = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function process_single_tag(tag_with_content, attributes = '', content = '', tag_offset) { |
|
const no_change = () => |
|
MappedCode.from_source(slice_source(tag_with_content, tag_offset, source)); |
|
if (!attributes && !content) return no_change(); |
|
const processed = await preprocessor({ |
|
content: content || '', |
|
attributes: parse_tag_attributes(attributes || ''), |
|
markup, |
|
filename |
|
}); |
|
if (!processed) return no_change(); |
|
if (processed.dependencies) dependencies.push(...processed.dependencies); |
|
if (!processed.map && processed.code === content) return no_change(); |
|
return processed_tag_to_code( |
|
processed, |
|
tag_name, |
|
attributes, |
|
stringify_tag_attributes(processed.attributes) ?? attributes, |
|
slice_source(content, tag_offset, source) |
|
); |
|
} |
|
const { string, map } = await replace_in_code(tag_regex, process_single_tag, source); |
|
return { string, map, dependencies }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function process_markup(process, source) { |
|
const processed = await process({ |
|
content: source.source, |
|
filename: source.filename |
|
}); |
|
if (processed) { |
|
return { |
|
string: processed.code, |
|
map: processed.map |
|
? |
|
typeof processed.map === 'string' |
|
? JSON.parse(processed.map) |
|
: processed.map |
|
: undefined, |
|
dependencies: processed.dependencies |
|
}; |
|
} else { |
|
return {}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default async function preprocess(source, preprocessor, options) { |
|
|
|
|
|
|
|
const filename = (options && options.filename) || (preprocessor).filename; |
|
const preprocessors = preprocessor |
|
? Array.isArray(preprocessor) |
|
? preprocessor |
|
: [preprocessor] |
|
: []; |
|
const result = new PreprocessResult(source, filename); |
|
|
|
|
|
|
|
for (const preprocessor of preprocessors) { |
|
if (preprocessor.markup) { |
|
result.update_source(await process_markup(preprocessor.markup, result)); |
|
} |
|
if (preprocessor.script) { |
|
result.update_source(await process_tag('script', preprocessor.script, result)); |
|
} |
|
if (preprocessor.style) { |
|
result.update_source(await process_tag('style', preprocessor.style, result)); |
|
} |
|
} |
|
|
|
return result.to_processed(); |
|
} |
|
|