use std::path::{Path, PathBuf};
use undercoat::{Options, compile_file};
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn vlq(segment: &str) -> Vec<i64> {
let (mut shift, mut value, mut out) = (0u32, i64::from(0), Vec::new());
for ch in segment.bytes() {
let digit = B64.iter().position(|&b| b == ch).expect("vlq 字符") as i64;
let cont = digit & 0x20 != 0;
value |= (digit & 0x1f) << shift;
shift += 5;
if !cont {
let neg = value & 1 != 0;
value >>= 1;
out.push(if neg { -value } else { value });
value = 0;
shift = 0;
}
}
out
}
fn line_mappings(map: &serde_json::Value) -> Vec<Option<(usize, u32, u32)>> {
let (mut file, mut line, mut col) = (0i64, 0i64, 0i64);
map["mappings"]
.as_str()
.expect("mappings")
.split(';')
.map(|row| {
let mut first = None;
for seg in row.split(',').filter(|s| !s.is_empty()) {
let v = vlq(seg);
if v.len() >= 4 {
file += v[1];
line += v[2];
col += v[3];
first.get_or_insert((file as usize, line as u32, col as u32));
}
}
first
})
.collect()
}
fn generated_line(css: &str, selector: &str) -> usize {
let head = format!("{selector} {{");
css.lines()
.position(|l| l.starts_with(&head))
.unwrap_or_else(|| panic!("产物里没有 {selector}:\n{css}"))
}
fn temp_dir(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos();
let dir = std::env::temp_dir().join(format!("undercoat-{tag}-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir temp");
dir
}
fn write(dir: &Path, name: &str, text: &str) {
std::fs::write(dir.join(name), text).expect("write fixture");
}
fn compile(dir: &Path, entry: &str) -> (String, serde_json::Value) {
let path = dir.join(entry);
let source = std::fs::read_to_string(&path).expect("read entry");
let out = compile_file(
&path,
&source,
&Options {
source_map: true,
project_root: Some(dir.to_string_lossy().to_string()),
..Default::default()
},
)
.expect("compile");
let map = out.map.expect("source map");
(out.css, serde_json::from_str(&map).expect("parse map"))
}
#[test]
fn extend_generated_selector_keeps_source_line() {
let dir = temp_dir("extend-map");
write(&dir, "main.less", "@import \"b.less\";\n");
write(&dir, "b.less", ".a { color: red; }\n.b { &:extend(.a); }\n");
let (css, map) = compile(&dir, "main.less");
assert_eq!(css, ".a,\n.b {\n color: red;\n}\n", "CSS = {css}");
let mappings = line_mappings(&map);
assert_eq!(mappings[0].expect("映射"), (1, 0, 0));
assert_eq!(mappings[1].expect("映射"), (1, 1, 0));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn import_maps_back_to_each_source_file() {
let dir = temp_dir("multi-map");
write(
&dir,
"main.less",
".a { x: 1 }\n@import \"b.less\";\n@import (multiple) \"b.less\";\n.c { z: 3 }\n",
);
write(&dir, "b.less", ".b1 { b: 2 }\n@import \"c.less\";\n");
write(&dir, "c.less", ".c1 { c: 3 }\n");
let (css, map) = compile(&dir, "main.less");
let sources: Vec<&str> = map["sources"]
.as_array()
.expect("sources")
.iter()
.map(|s| s.as_str().expect("source"))
.collect();
assert_eq!(sources, ["main.less", "b.less", "c.less"], "按首次出现顺序");
let contents = map["sourcesContent"].as_array().expect("sourcesContent");
assert_eq!(contents.len(), sources.len());
assert!(contents[1].as_str().expect("b 原文").contains(".b1"));
let mappings = line_mappings(&map);
let at = |selector: &str| mappings[generated_line(&css, selector)].expect("映射");
assert_eq!(at(".a"), (0, 0, 0));
assert_eq!(at(".c"), (0, 3, 0));
assert_eq!(at(".b1"), (1, 0, 0));
assert_eq!(at(".c1"), (2, 0, 0));
let b1: Vec<usize> = css
.lines()
.enumerate()
.filter(|(_, l)| l.starts_with(".b1"))
.map(|(i, _)| i)
.collect();
assert_eq!(b1.len(), 2, "产物应有两份 .b1:\n{css}");
for line in b1 {
assert_eq!(mappings[line].expect("映射"), (1, 0, 0));
}
std::fs::remove_dir_all(&dir).ok();
}