Merge branch 'master' into node-fields
This commit is contained in:
commit
f52271352b
26 changed files with 2655 additions and 233 deletions
69
cli/src/config.rs
Normal file
69
cli/src/config.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use super::highlight::Theme;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs, io};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
#[serde(skip)]
|
||||
pub binary_directory: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(rename = "parser-directories")]
|
||||
pub parser_directories: Vec<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn get_path(home_dir: &Path) -> PathBuf {
|
||||
env::var("TREE_SITTER_DIR")
|
||||
.map(|p| p.into())
|
||||
.unwrap_or_else(|_| home_dir.join(".tree-sitter"))
|
||||
}
|
||||
|
||||
pub fn load(home_dir: &Path) -> Self {
|
||||
let tree_sitter_dir = Self::get_path(home_dir);
|
||||
let config_path = tree_sitter_dir.join("config.json");
|
||||
let mut result = fs::read_to_string(&config_path)
|
||||
.map_err(drop)
|
||||
.and_then(|json| serde_json::from_str(&json).map_err(drop))
|
||||
.unwrap_or_else(|_| Self::default());
|
||||
result.init(home_dir, &tree_sitter_dir);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn save(&self, home_dir: &Path) -> io::Result<()> {
|
||||
let tree_sitter_dir = Self::get_path(home_dir);
|
||||
let config_path = tree_sitter_dir.join("config.json");
|
||||
let json = serde_json::to_string_pretty(self).expect("Failed to serialize config");
|
||||
fs::write(config_path, json)
|
||||
}
|
||||
|
||||
pub fn new(home_dir: &Path) -> Self {
|
||||
let tree_sitter_dir = Self::get_path(home_dir);
|
||||
let mut result = Self::default();
|
||||
result.init(home_dir, &tree_sitter_dir);
|
||||
result
|
||||
}
|
||||
|
||||
fn init(&mut self, home_dir: &Path, tree_sitter_dir: &Path) {
|
||||
if self.parser_directories.is_empty() {
|
||||
self.parser_directories = vec![
|
||||
home_dir.join("github"),
|
||||
home_dir.join("src"),
|
||||
home_dir.join("source"),
|
||||
]
|
||||
}
|
||||
|
||||
let binary_path = tree_sitter_dir.join("bin");
|
||||
self.binary_directory = binary_path;
|
||||
fs::create_dir_all(&self.binary_directory).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Could not find or create parser binary directory {:?}. Error: {}",
|
||||
self.binary_directory, error
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
use std::io;
|
||||
use tree_sitter_highlight::PropertySheetError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error(pub String);
|
||||
|
|
@ -42,3 +43,13 @@ impl From<String> for Error {
|
|||
Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PropertySheetError> for Error {
|
||||
fn from(error: PropertySheetError) -> Self {
|
||||
match error {
|
||||
PropertySheetError::InvalidFormat(e) => Self::from(e),
|
||||
PropertySheetError::InvalidRegex(e) => Self::regex(&e.to_string()),
|
||||
PropertySheetError::InvalidJSON(e) => Self::from(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
const UNICODE_ESCAPE_PATTERN = /\\u([0-9a-f]{4})/gi;
|
||||
|
||||
function alias(rule, value) {
|
||||
const result = {
|
||||
type: "ALIAS",
|
||||
|
|
@ -180,12 +178,8 @@ function normalize(value) {
|
|||
};
|
||||
case RegExp:
|
||||
return {
|
||||
type: 'PATTERN',
|
||||
value: value.source
|
||||
.replace(
|
||||
UNICODE_ESCAPE_PATTERN,
|
||||
(match, group) => String.fromCharCode(parseInt(group, 16))
|
||||
)
|
||||
type: 'PATTERN',
|
||||
value: value.source
|
||||
};
|
||||
case ReferenceError:
|
||||
throw value
|
||||
|
|
|
|||
373
cli/src/highlight.rs
Normal file
373
cli/src/highlight.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
use crate::error::Result;
|
||||
use crate::loader::Loader;
|
||||
use ansi_term::{Color, Style};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::{fmt, fs, io, path};
|
||||
use tree_sitter::{Language, PropertySheet};
|
||||
use tree_sitter_highlight::{highlight, highlight_html, HighlightEvent, Properties, Scope};
|
||||
|
||||
lazy_static! {
|
||||
static ref CSS_STYLES_BY_COLOR_ID: Vec<String> =
|
||||
serde_json::from_str(include_str!("../vendor/xterm-colors.json")).unwrap();
|
||||
}
|
||||
|
||||
pub struct Theme {
|
||||
ansi_styles: Vec<Option<Style>>,
|
||||
css_styles: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn load(path: &path::Path) -> io::Result<Self> {
|
||||
let json = fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&json).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn ansi_style(&self, scope: Scope) -> Option<&Style> {
|
||||
self.ansi_styles[scope as usize].as_ref()
|
||||
}
|
||||
|
||||
fn css_style(&self, scope: Scope) -> Option<&str> {
|
||||
self.css_styles[scope as usize].as_ref().map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Theme {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Theme, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let scope_count = Scope::Unknown as usize + 1;
|
||||
let mut ansi_styles = vec![None; scope_count];
|
||||
let mut css_styles = vec![None; scope_count];
|
||||
if let Ok(colors) = HashMap::<Scope, Value>::deserialize(deserializer) {
|
||||
for (scope, style_value) in colors {
|
||||
let mut style = Style::default();
|
||||
parse_style(&mut style, style_value);
|
||||
ansi_styles[scope as usize] = Some(style);
|
||||
css_styles[scope as usize] = Some(style_to_css(style));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
ansi_styles,
|
||||
css_styles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Theme {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let entry_count = self.ansi_styles.iter().filter(|i| i.is_some()).count();
|
||||
let mut map = serializer.serialize_map(Some(entry_count))?;
|
||||
for (i, style) in self.ansi_styles.iter().enumerate() {
|
||||
let scope = Scope::from_usize(i).unwrap();
|
||||
if scope == Scope::Unknown {
|
||||
break;
|
||||
}
|
||||
if let Some(style) = style {
|
||||
let color = style.foreground.map(|color| match color {
|
||||
Color::Black => json!("black"),
|
||||
Color::Blue => json!("blue"),
|
||||
Color::Cyan => json!("cyan"),
|
||||
Color::Green => json!("green"),
|
||||
Color::Purple => json!("purple"),
|
||||
Color::Red => json!("red"),
|
||||
Color::White => json!("white"),
|
||||
Color::Yellow => json!("yellow"),
|
||||
Color::RGB(r, g, b) => json!(format!("#{:x?}{:x?}{:x?}", r, g, b)),
|
||||
Color::Fixed(n) => json!(n),
|
||||
});
|
||||
if style.is_bold || style.is_italic || style.is_underline {
|
||||
let mut entry = HashMap::new();
|
||||
if let Some(color) = color {
|
||||
entry.insert("color", color);
|
||||
}
|
||||
if style.is_bold {
|
||||
entry.insert("bold", Value::Bool(true));
|
||||
}
|
||||
if style.is_italic {
|
||||
entry.insert("italic", Value::Bool(true));
|
||||
}
|
||||
if style.is_underline {
|
||||
entry.insert("underline", Value::Bool(true));
|
||||
}
|
||||
map.serialize_entry(&scope, &entry)?;
|
||||
} else if let Some(color) = color {
|
||||
map.serialize_entry(&scope, &color)?;
|
||||
} else {
|
||||
map.serialize_entry(&scope, &Value::Null)?;
|
||||
}
|
||||
} else {
|
||||
map.serialize_entry(&scope, &Value::Null)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
serde_json::from_str(
|
||||
r#"
|
||||
{
|
||||
"attribute": {"color": 124, "italic": true},
|
||||
"comment": {"color": 245, "italic": true},
|
||||
"constant.builtin": {"color": 94, "bold": true},
|
||||
"constant": 94,
|
||||
"constructor": 136,
|
||||
"embedded": null,
|
||||
"function.builtin": {"color": 26, "bold": true},
|
||||
"function": 26,
|
||||
"keyword": 56,
|
||||
"number": {"color": 94, "bold": true},
|
||||
"property": 124,
|
||||
"operator": {"color": 239, "bold": true},
|
||||
"punctuation.bracket": 239,
|
||||
"punctuation.delimiter": 239,
|
||||
"string.special": 30,
|
||||
"string": 28,
|
||||
"tag": 18,
|
||||
"type": 23,
|
||||
"type.builtin": {"color": 23, "bold": true},
|
||||
"variable.builtin": {"bold": true}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Theme {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{{")?;
|
||||
let mut first = true;
|
||||
for (i, style) in self.ansi_styles.iter().enumerate() {
|
||||
if let Some(style) = style {
|
||||
let scope = Scope::from_usize(i).unwrap();
|
||||
if !first {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{:?}: {:?}", scope, style)?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
write!(f, "}}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_style(style: &mut Style, json: Value) {
|
||||
if let Value::Object(entries) = json {
|
||||
for (property_name, value) in entries {
|
||||
match property_name.as_str() {
|
||||
"bold" => *style = style.bold(),
|
||||
"italic" => *style = style.italic(),
|
||||
"underline" => *style = style.underline(),
|
||||
"color" => {
|
||||
if let Some(color) = parse_color(value) {
|
||||
*style = style.fg(color);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else if let Some(color) = parse_color(json) {
|
||||
*style = style.fg(color);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(json: Value) -> Option<Color> {
|
||||
match json {
|
||||
Value::Number(n) => match n.as_u64() {
|
||||
Some(n) => Some(Color::Fixed(n as u8)),
|
||||
_ => None,
|
||||
},
|
||||
Value::String(s) => match s.to_lowercase().as_str() {
|
||||
"black" => Some(Color::Black),
|
||||
"blue" => Some(Color::Blue),
|
||||
"cyan" => Some(Color::Cyan),
|
||||
"green" => Some(Color::Green),
|
||||
"purple" => Some(Color::Purple),
|
||||
"red" => Some(Color::Red),
|
||||
"white" => Some(Color::White),
|
||||
"yellow" => Some(Color::Yellow),
|
||||
s => {
|
||||
if s.starts_with("#") && s.len() >= 7 {
|
||||
if let (Ok(red), Ok(green), Ok(blue)) = (
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
) {
|
||||
Some(Color::RGB(red, green, blue))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn style_to_css(style: Style) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut result = "style='".to_string();
|
||||
if style.is_bold {
|
||||
write!(&mut result, "font-weight: bold;").unwrap();
|
||||
}
|
||||
if style.is_italic {
|
||||
write!(&mut result, "font-style: italic;").unwrap();
|
||||
}
|
||||
if let Some(color) = style.foreground {
|
||||
write!(&mut result, "color: {};", color_to_css(color)).unwrap();
|
||||
}
|
||||
result.push('\'');
|
||||
result
|
||||
}
|
||||
|
||||
fn color_to_css(color: Color) -> &'static str {
|
||||
match color {
|
||||
Color::Black => "black",
|
||||
Color::Blue => "blue",
|
||||
Color::Red => "red",
|
||||
Color::Green => "green",
|
||||
Color::Yellow => "yellow",
|
||||
Color::Cyan => "cyan",
|
||||
Color::Purple => "purple",
|
||||
Color::White => "white",
|
||||
Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(),
|
||||
_ => panic!("Unsupported color type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ansi(
|
||||
loader: &Loader,
|
||||
theme: &Theme,
|
||||
source: &[u8],
|
||||
language: Language,
|
||||
property_sheet: &PropertySheet<Properties>,
|
||||
) -> Result<()> {
|
||||
use std::io::Write;
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
let mut scope_stack = Vec::new();
|
||||
for event in highlight(source, language, property_sheet, |s| {
|
||||
language_for_injection_string(loader, s)
|
||||
})? {
|
||||
match event {
|
||||
HighlightEvent::Source(s) => {
|
||||
if let Some(style) = scope_stack.last().and_then(|s| theme.ansi_style(*s)) {
|
||||
write!(&mut stdout, "{}", style.paint(s))?;
|
||||
} else {
|
||||
write!(&mut stdout, "{}", s)?;
|
||||
}
|
||||
}
|
||||
HighlightEvent::ScopeStart(s) => {
|
||||
scope_stack.push(s);
|
||||
}
|
||||
HighlightEvent::ScopeEnd => {
|
||||
scope_stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const HTML_HEADER: &'static str = "
|
||||
<!doctype HTML>
|
||||
<head>
|
||||
<title>Tree-sitter Highlighting</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace
|
||||
}
|
||||
.line-number {
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
color: rgba(27,31,35,.3);
|
||||
padding: 0 10px;
|
||||
}
|
||||
.line {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
";
|
||||
|
||||
pub const HTML_FOOTER: &'static str = "
|
||||
</body>
|
||||
";
|
||||
|
||||
pub fn html(
|
||||
loader: &Loader,
|
||||
theme: &Theme,
|
||||
source: &[u8],
|
||||
language: Language,
|
||||
property_sheet: &PropertySheet<Properties>,
|
||||
) -> Result<()> {
|
||||
use std::io::Write;
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
write!(&mut stdout, "<table>\n")?;
|
||||
let lines = highlight_html(
|
||||
source,
|
||||
language,
|
||||
property_sheet,
|
||||
|s| language_for_injection_string(loader, s),
|
||||
|scope| {
|
||||
if let Some(css_style) = theme.css_style(scope) {
|
||||
css_style
|
||||
} else {
|
||||
""
|
||||
}
|
||||
},
|
||||
)?;
|
||||
for (i, line) in lines.into_iter().enumerate() {
|
||||
write!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{}</td></tr>\n",
|
||||
i + 1,
|
||||
line
|
||||
)?;
|
||||
}
|
||||
write!(&mut stdout, "</table>\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn language_for_injection_string<'a>(
|
||||
loader: &'a Loader,
|
||||
string: &str,
|
||||
) -> Option<(Language, &'a PropertySheet<Properties>)> {
|
||||
match loader.language_configuration_for_injection_string(string) {
|
||||
Err(message) => {
|
||||
eprintln!(
|
||||
"Failed to load language for injection string '{}': {}",
|
||||
string, message.0
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some((language, configuration))) => {
|
||||
match configuration.highlight_property_sheet(language) {
|
||||
Err(message) => {
|
||||
eprintln!(
|
||||
"Failed to load property sheet for injection string '{}': {}",
|
||||
string, message.0
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some(sheet)) => Some((language, sheet)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod generate;
|
||||
pub mod highlight;
|
||||
pub mod loader;
|
||||
pub mod logger;
|
||||
pub mod parse;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::error::{Error, Result};
|
||||
use libloading::{Library, Symbol};
|
||||
use once_cell::unsync::OnceCell;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -9,6 +10,7 @@ use std::process::Command;
|
|||
use std::time::SystemTime;
|
||||
use std::{fs, mem};
|
||||
use tree_sitter::{Language, PropertySheet};
|
||||
use tree_sitter_highlight::{load_property_sheet, Properties};
|
||||
|
||||
#[cfg(unix)]
|
||||
const DYLIB_EXTENSION: &'static str = "so";
|
||||
|
|
@ -20,16 +22,18 @@ const BUILD_TARGET: &'static str = env!("BUILD_TARGET");
|
|||
|
||||
struct LanguageRepo {
|
||||
path: PathBuf,
|
||||
language: Option<Language>,
|
||||
language: OnceCell<Language>,
|
||||
configurations: Vec<LanguageConfiguration>,
|
||||
}
|
||||
|
||||
pub struct LanguageConfiguration {
|
||||
_name: String,
|
||||
scope: Option<String>,
|
||||
_content_regex: Option<Regex>,
|
||||
_first_line_regex: Option<Regex>,
|
||||
injection_regex: Option<Regex>,
|
||||
file_types: Vec<String>,
|
||||
_highlight_property_sheet: Option<std::result::Result<PropertySheet, PathBuf>>,
|
||||
highlight_property_sheet_path: Option<PathBuf>,
|
||||
highlight_property_sheet: OnceCell<Option<PropertySheet<Properties>>>,
|
||||
}
|
||||
|
||||
pub struct Loader {
|
||||
|
|
@ -75,8 +79,23 @@ impl Loader {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn language_configuration_for_scope(
|
||||
&self,
|
||||
scope: &str,
|
||||
) -> Result<Option<(Language, &LanguageConfiguration)>> {
|
||||
for (i, repo) in self.language_repos.iter().enumerate() {
|
||||
for configuration in &repo.configurations {
|
||||
if configuration.scope.as_ref().map_or(false, |s| s == scope) {
|
||||
let (language, _) = self.language_configuration_for_id(i)?;
|
||||
return Ok(Some((language, &configuration)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn language_configuration_for_file_name(
|
||||
&mut self,
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Option<(Language, &LanguageConfiguration)>> {
|
||||
let ids = path
|
||||
|
|
@ -100,20 +119,43 @@ impl Loader {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn language_configuration_for_injection_string(
|
||||
&self,
|
||||
string: &str,
|
||||
) -> Result<Option<(Language, &LanguageConfiguration)>> {
|
||||
let mut best_match_length = 0;
|
||||
let mut best_match_position = None;
|
||||
for (i, repo) in self.language_repos.iter().enumerate() {
|
||||
for (j, configuration) in repo.configurations.iter().enumerate() {
|
||||
if let Some(injection_regex) = &configuration.injection_regex {
|
||||
if let Some(mat) = injection_regex.find(string) {
|
||||
let length = mat.end() - mat.start();
|
||||
if length > best_match_length {
|
||||
best_match_position = Some((i, j));
|
||||
best_match_length = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((i, j)) = best_match_position {
|
||||
let (language, configurations) = self.language_configuration_for_id(i)?;
|
||||
Ok(Some((language, &configurations[j])))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn language_configuration_for_id(
|
||||
&mut self,
|
||||
&self,
|
||||
id: usize,
|
||||
) -> Result<(Language, &Vec<LanguageConfiguration>)> {
|
||||
let repo = &self.language_repos[id];
|
||||
let language = if let Some(language) = repo.language {
|
||||
language
|
||||
} else {
|
||||
let language = repo.language.get_or_try_init(|| {
|
||||
let src_path = repo.path.join("src");
|
||||
let language = self.load_language_at_path(&src_path, &src_path)?;
|
||||
self.language_repos[id].language = Some(language);
|
||||
language
|
||||
};
|
||||
Ok((language, &self.language_repos[id].configurations))
|
||||
self.load_language_at_path(&src_path, &src_path)
|
||||
})?;
|
||||
Ok((*language, &self.language_repos[id].configurations))
|
||||
}
|
||||
|
||||
pub fn load_language_at_path(&self, src_path: &Path, header_path: &Path) -> Result<Language> {
|
||||
|
|
@ -191,7 +233,8 @@ impl Loader {
|
|||
.arg("-I")
|
||||
.arg(header_path)
|
||||
.arg("-o")
|
||||
.arg(&library_path);
|
||||
.arg(&library_path)
|
||||
.arg("-O2");
|
||||
if let Some(scanner_path) = scanner_path.as_ref() {
|
||||
if scanner_path.extension() == Some("c".as_ref()) {
|
||||
command.arg("-xc").arg("-std=c99").arg(scanner_path);
|
||||
|
|
@ -231,13 +274,15 @@ impl Loader {
|
|||
fn find_language_at_path<'a>(&'a mut self, parser_path: &Path) -> Result<usize> {
|
||||
#[derive(Deserialize)]
|
||||
struct LanguageConfigurationJSON {
|
||||
name: String,
|
||||
scope: Option<String>,
|
||||
#[serde(rename = "file-types")]
|
||||
file_types: Option<Vec<String>>,
|
||||
#[serde(rename = "content-regex")]
|
||||
content_regex: Option<String>,
|
||||
#[serde(rename = "first-line-regex")]
|
||||
first_line_regex: Option<String>,
|
||||
#[serde(rename = "injection-regex")]
|
||||
injection_regex: Option<String>,
|
||||
highlights: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +300,7 @@ impl Loader {
|
|||
configurations
|
||||
.into_iter()
|
||||
.map(|conf| LanguageConfiguration {
|
||||
_name: conf.name,
|
||||
scope: conf.scope,
|
||||
file_types: conf.file_types.unwrap_or(Vec::new()),
|
||||
_content_regex: conf
|
||||
.content_regex
|
||||
|
|
@ -263,7 +308,11 @@ impl Loader {
|
|||
_first_line_regex: conf
|
||||
.first_line_regex
|
||||
.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok()),
|
||||
_highlight_property_sheet: conf.highlights.map(|d| Err(d.into())),
|
||||
injection_regex: conf
|
||||
.injection_regex
|
||||
.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok()),
|
||||
highlight_property_sheet_path: conf.highlights.map(|h| parser_path.join(h)),
|
||||
highlight_property_sheet: OnceCell::new(),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
|
@ -279,7 +328,7 @@ impl Loader {
|
|||
|
||||
self.language_repos.push(LanguageRepo {
|
||||
path: parser_path.to_owned(),
|
||||
language: None,
|
||||
language: OnceCell::new(),
|
||||
configurations,
|
||||
});
|
||||
|
||||
|
|
@ -287,6 +336,25 @@ impl Loader {
|
|||
}
|
||||
}
|
||||
|
||||
impl LanguageConfiguration {
|
||||
pub fn highlight_property_sheet(
|
||||
&self,
|
||||
language: Language,
|
||||
) -> Result<Option<&PropertySheet<Properties>>> {
|
||||
self.highlight_property_sheet
|
||||
.get_or_try_init(|| {
|
||||
if let Some(path) = &self.highlight_property_sheet_path {
|
||||
let sheet_json = fs::read_to_string(path)?;
|
||||
let sheet = load_property_sheet(language, &sheet_json)?;
|
||||
Ok(Some(sheet))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.map(Option::as_ref)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_recompile(
|
||||
lib_path: &Path,
|
||||
parser_c_path: &Path,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ use std::fs;
|
|||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
use std::usize;
|
||||
use tree_sitter_cli::loader::Loader;
|
||||
use tree_sitter_cli::{error, generate, logger, parse, properties, test};
|
||||
use tree_sitter_cli::{
|
||||
config, error, generate, highlight, loader, logger, parse, properties, test,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
|
|
@ -25,6 +26,7 @@ fn run() -> error::Result<()> {
|
|||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.author("Max Brunsfeld <maxbrunsfeld@gmail.com>")
|
||||
.about("Generates and tests parsers")
|
||||
.subcommand(SubCommand::with_name("init-config").about("Generate a default config file"))
|
||||
.subcommand(
|
||||
SubCommand::with_name("generate")
|
||||
.about("Generate a parser")
|
||||
|
|
@ -64,16 +66,29 @@ fn run() -> error::Result<()> {
|
|||
.arg(Arg::with_name("debug").long("debug").short("d"))
|
||||
.arg(Arg::with_name("debug-graph").long("debug-graph").short("D")),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("highlight")
|
||||
.about("Highlight a file")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.index(1)
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
)
|
||||
.arg(Arg::with_name("scope").long("scope").takes_value(true))
|
||||
.arg(Arg::with_name("html").long("html").short("h")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let home_dir = dirs::home_dir().unwrap();
|
||||
let home_dir = dirs::home_dir().expect("Failed to read home directory");
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
let config_dir = home_dir.join(".tree-sitter");
|
||||
let config = config::Config::load(&home_dir);
|
||||
let mut loader = loader::Loader::new(config.binary_directory.clone());
|
||||
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let mut loader = Loader::new(config_dir);
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("generate") {
|
||||
if matches.subcommand_matches("init-config").is_some() {
|
||||
let config = config::Config::new(&home_dir);
|
||||
config.save(&home_dir)?;
|
||||
} else if let Some(matches) = matches.subcommand_matches("generate") {
|
||||
if matches.is_present("log") {
|
||||
logger::init();
|
||||
}
|
||||
|
|
@ -81,12 +96,14 @@ fn run() -> error::Result<()> {
|
|||
let grammar_path = matches.value_of("grammar-path");
|
||||
let minimize = !matches.is_present("no-minimize");
|
||||
let properties_only = matches.is_present("properties-only");
|
||||
let parser_only = grammar_path.is_some();
|
||||
let state_ids_to_log = matches
|
||||
.values_of("state-ids-to-log")
|
||||
.map_or(Vec::new(), |ids| {
|
||||
ids.filter_map(|id| usize::from_str_radix(id, 10).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if !properties_only {
|
||||
generate::generate_parser_in_directory(
|
||||
¤t_dir,
|
||||
|
|
@ -95,7 +112,10 @@ fn run() -> error::Result<()> {
|
|||
state_ids_to_log,
|
||||
)?;
|
||||
}
|
||||
properties::generate_property_sheets_in_directory(¤t_dir)?;
|
||||
|
||||
if !parser_only {
|
||||
properties::generate_property_sheets_in_directory(¤t_dir)?;
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("test") {
|
||||
let debug = matches.is_present("debug");
|
||||
let debug_graph = matches.is_present("debug-graph");
|
||||
|
|
@ -111,7 +131,7 @@ fn run() -> error::Result<()> {
|
|||
let debug_graph = matches.is_present("debug-graph");
|
||||
let quiet = matches.is_present("quiet");
|
||||
let time = matches.is_present("time");
|
||||
loader.find_all_languages(&vec![home_dir.join("github")])?;
|
||||
loader.find_all_languages(&config.parser_directories)?;
|
||||
let paths = matches
|
||||
.values_of("path")
|
||||
.unwrap()
|
||||
|
|
@ -144,6 +164,51 @@ fn run() -> error::Result<()> {
|
|||
if has_error {
|
||||
return Err(error::Error(String::new()));
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("highlight") {
|
||||
let paths = matches.values_of("path").unwrap().into_iter();
|
||||
let html_mode = matches.is_present("html");
|
||||
loader.find_all_languages(&config.parser_directories)?;
|
||||
|
||||
if html_mode {
|
||||
println!("{}", highlight::HTML_HEADER);
|
||||
}
|
||||
|
||||
let language_config;
|
||||
if let Some(scope) = matches.value_of("scope") {
|
||||
language_config = loader.language_configuration_for_scope(scope)?;
|
||||
if language_config.is_none() {
|
||||
return Err(error::Error(format!("Unknown scope '{}'", scope)));
|
||||
}
|
||||
} else {
|
||||
language_config = None;
|
||||
}
|
||||
|
||||
for path in paths {
|
||||
let path = Path::new(path);
|
||||
let (language, language_config) = match language_config {
|
||||
Some(v) => v,
|
||||
None => match loader.language_configuration_for_file_name(path)? {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
eprintln!("No language found for path {:?}", path);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(sheet) = language_config.highlight_property_sheet(language)? {
|
||||
let source = fs::read(path)?;
|
||||
if html_mode {
|
||||
highlight::html(&loader, &config.theme, &source, language, sheet)?;
|
||||
} else {
|
||||
highlight::ansi(&loader, &config.theme, &source, language, sheet)?;
|
||||
}
|
||||
} else {
|
||||
return Err(error::Error(format!(
|
||||
"No syntax highlighting property sheet specified"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::error::Result;
|
||||
use super::error::{Error, Result};
|
||||
use super::util;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
|
@ -18,7 +18,8 @@ pub fn parse_file_at_path(
|
|||
let mut _log_session = None;
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(language)?;
|
||||
let source_code = fs::read(path)?;
|
||||
let source_code = fs::read(path)
|
||||
.map_err(|e| Error(format!("Error reading source file {:?}: {}", path, e)))?;
|
||||
|
||||
if debug_graph {
|
||||
_log_session = Some(util::log_graphs(&mut parser, "log.html")?);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use rsass::sass::Value;
|
|||
use rsass::selectors::SelectorPart;
|
||||
use serde_derive::Serialize;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{btree_map, BTreeMap, HashMap, VecDeque};
|
||||
use std::fmt::{self, Write};
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufWriter;
|
||||
|
|
@ -15,12 +15,13 @@ use tree_sitter::{self, PropertyStateJSON, PropertyTransitionJSON};
|
|||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum PropertyValue {
|
||||
Number(isize),
|
||||
String(String),
|
||||
Object(PropertySet),
|
||||
Array(Vec<PropertyValue>),
|
||||
}
|
||||
|
||||
type PropertySet = HashMap<String, PropertyValue>;
|
||||
type PropertySet = BTreeMap<String, PropertyValue>;
|
||||
type PropertySheetJSON = tree_sitter::PropertySheetJSON<PropertySet>;
|
||||
type StateId = usize;
|
||||
type PropertySetId = usize;
|
||||
|
|
@ -160,7 +161,7 @@ impl Builder {
|
|||
}
|
||||
|
||||
fn populate_state(&mut self, item_set: ItemSet, state_id: StateId) {
|
||||
let mut transition_map: HashSet<(PropertyTransitionJSON, u32)> = HashSet::new();
|
||||
let mut transitions: HashMap<PropertyTransitionJSON, u32> = HashMap::new();
|
||||
let mut selector_matches = Vec::new();
|
||||
|
||||
// First, compute all of the possible state transition predicates for
|
||||
|
|
@ -173,18 +174,21 @@ impl Builder {
|
|||
// If this item has more elements remaining in its selector, then
|
||||
// add a state transition based on the next step.
|
||||
if let Some(step) = next_step {
|
||||
transition_map.insert((
|
||||
PropertyTransitionJSON {
|
||||
transitions
|
||||
.entry(PropertyTransitionJSON {
|
||||
kind: step.kind.clone(),
|
||||
field: step.field.clone(),
|
||||
named: step.is_named,
|
||||
index: step.child_index,
|
||||
text: step.text_pattern.clone(),
|
||||
state_id: 0,
|
||||
},
|
||||
// Include the rule id so that it can be used when sorting transitions.
|
||||
item.rule_id,
|
||||
));
|
||||
})
|
||||
.and_modify(|rule_id| {
|
||||
if item.rule_id > *rule_id {
|
||||
*rule_id = item.rule_id;
|
||||
}
|
||||
})
|
||||
.or_insert(item.rule_id);
|
||||
}
|
||||
// If the item has matched its entire selector, then the item's
|
||||
// properties are applicable to this state.
|
||||
|
|
@ -196,46 +200,11 @@ impl Builder {
|
|||
}
|
||||
}
|
||||
|
||||
// For eacy possible state transition, compute the set of items in that transition's
|
||||
// destination state.
|
||||
let mut transition_list: Vec<(PropertyTransitionJSON, u32)> = transition_map
|
||||
.into_iter()
|
||||
.map(|(mut transition, rule_id)| {
|
||||
let mut next_item_set = ItemSet::new();
|
||||
for item in &item_set {
|
||||
let rule = &self.rules[item.rule_id as usize];
|
||||
let selector = &rule.selectors[item.selector_id as usize];
|
||||
let next_step = selector.0.get(item.step_id as usize);
|
||||
|
||||
if let Some(step) = next_step {
|
||||
// If the next step of the item's selector satisfies this transition,
|
||||
// advance the item to the next part of its selector and add the
|
||||
// resulting item to this transition's destination state.
|
||||
if step_matches_transition(step, &transition) {
|
||||
next_item_set.insert(Item {
|
||||
rule_id: item.rule_id,
|
||||
selector_id: item.selector_id,
|
||||
step_id: item.step_id + 1,
|
||||
});
|
||||
}
|
||||
|
||||
// If the next step of the item is not an immediate child, then
|
||||
// include this item in this transition's destination state, because
|
||||
// the next step of the item might match a descendant node.
|
||||
if !step.is_immediate {
|
||||
next_item_set.insert(*item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.state_id = self.add_state(next_item_set);
|
||||
(transition, rule_id)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Ensure that for a given node type, more specific transitions are tried
|
||||
// first, and in the event of a tie, transitions corresponding to later rules
|
||||
// in the cascade are tried first.
|
||||
let mut transition_list: Vec<(PropertyTransitionJSON, u32)> =
|
||||
transitions.into_iter().collect();
|
||||
transition_list.sort_by(|a, b| {
|
||||
(transition_specificity(&b.0).cmp(&transition_specificity(&a.0)))
|
||||
.then_with(|| b.1.cmp(&a.1))
|
||||
|
|
@ -244,6 +213,39 @@ impl Builder {
|
|||
.then_with(|| a.0.field.cmp(&b.0.field))
|
||||
});
|
||||
|
||||
// For eacy possible state transition, compute the set of items in that transition's
|
||||
// destination state.
|
||||
for (transition, _) in transition_list.iter_mut() {
|
||||
let mut next_item_set = ItemSet::new();
|
||||
for item in &item_set {
|
||||
let rule = &self.rules[item.rule_id as usize];
|
||||
let selector = &rule.selectors[item.selector_id as usize];
|
||||
let next_step = selector.0.get(item.step_id as usize);
|
||||
|
||||
if let Some(step) = next_step {
|
||||
// If the next step of the item's selector satisfies this transition,
|
||||
// advance the item to the next part of its selector and add the
|
||||
// resulting item to this transition's destination state.
|
||||
if step_matches_transition(step, &transition) {
|
||||
next_item_set.insert(Item {
|
||||
rule_id: item.rule_id,
|
||||
selector_id: item.selector_id,
|
||||
step_id: item.step_id + 1,
|
||||
});
|
||||
}
|
||||
|
||||
// If the next step of the item is not an immediate child, then
|
||||
// include this item in this transition's destination state, because
|
||||
// the next step of the item might match a descendant node.
|
||||
if !step.is_immediate {
|
||||
next_item_set.insert(*item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.state_id = self.add_state(next_item_set);
|
||||
}
|
||||
|
||||
// Compute the merged properties that apply in the current state.
|
||||
// Sort the matching property sets by ascending specificity and by
|
||||
// their order in the sheet. This way, more specific selectors and later
|
||||
|
|
@ -475,38 +477,9 @@ fn generate_property_sheet(path: impl AsRef<Path>, css: &str) -> Result<Property
|
|||
}
|
||||
|
||||
fn parse_property_sheet(path: &Path, css: &str) -> Result<Vec<Rule>> {
|
||||
let mut i = 0;
|
||||
let mut schema_paths = Vec::new();
|
||||
let mut items = rsass::parse_scss_data(css.as_bytes())?;
|
||||
while i < items.len() {
|
||||
match &items[i] {
|
||||
rsass::Item::Import(arg) => {
|
||||
if let Some(s) = get_sass_string(arg) {
|
||||
let import_path = resolve_path(path, s)?;
|
||||
let imported_items = rsass::parse_scss_file(&import_path)?;
|
||||
items.splice(i..(i + 1), imported_items);
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error("@import arguments must be strings".to_string()));
|
||||
}
|
||||
}
|
||||
rsass::Item::AtRule { name, args, .. } => match name.as_str() {
|
||||
"schema" => {
|
||||
if let Some(s) = get_sass_string(args) {
|
||||
// TODO - use schema
|
||||
let _schema_path = resolve_path(path, s)?;
|
||||
items.remove(i);
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error("@schema arguments must be strings".to_string()));
|
||||
}
|
||||
}
|
||||
_ => return Err(Error(format!("Unsupported at-rule '{}'", name))),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
process_at_rules(&mut items, &mut schema_paths, path)?;
|
||||
let mut result = Vec::new();
|
||||
let selector_prefixes = vec![Vec::new()];
|
||||
parse_sass_items(items, &selector_prefixes, &mut result)?;
|
||||
|
|
@ -525,10 +498,10 @@ fn parse_sass_items(
|
|||
rsass::Item::Property(name, value) => {
|
||||
let value = parse_sass_value(&value)?;
|
||||
match properties.entry(name.to_string()) {
|
||||
Entry::Vacant(v) => {
|
||||
btree_map::Entry::Vacant(v) => {
|
||||
v.insert(value);
|
||||
}
|
||||
Entry::Occupied(mut o) => {
|
||||
btree_map::Entry::Occupied(mut o) => {
|
||||
let existing_value = o.get_mut();
|
||||
if let PropertyValue::Array(items) = existing_value {
|
||||
items.push(value);
|
||||
|
|
@ -693,6 +666,45 @@ fn parse_sass_items(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn process_at_rules(
|
||||
items: &mut Vec<rsass::Item>,
|
||||
schema_paths: &mut Vec<PathBuf>,
|
||||
path: &Path,
|
||||
) -> Result<()> {
|
||||
let mut i = 0;
|
||||
while i < items.len() {
|
||||
match &items[i] {
|
||||
rsass::Item::Import(arg) => {
|
||||
if let Some(s) = get_sass_string(arg) {
|
||||
let import_path = resolve_path(path, s)?;
|
||||
let mut imported_items = rsass::parse_scss_file(&import_path)?;
|
||||
process_at_rules(&mut imported_items, schema_paths, &import_path)?;
|
||||
items.splice(i..(i + 1), imported_items);
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error("@import arguments must be strings".to_string()));
|
||||
}
|
||||
}
|
||||
rsass::Item::AtRule { name, args, .. } => match name.as_str() {
|
||||
"schema" => {
|
||||
if let Some(s) = get_sass_string(args) {
|
||||
let schema_path = resolve_path(path, s)?;
|
||||
schema_paths.push(schema_path);
|
||||
items.remove(i);
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error("@schema arguments must be strings".to_string()));
|
||||
}
|
||||
}
|
||||
_ => return Err(Error(format!("Unsupported at-rule '{}'", name))),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_sass_value(value: &Value) -> Result<PropertyValue> {
|
||||
match value {
|
||||
Value::Literal(s) => {
|
||||
|
|
@ -724,7 +736,7 @@ fn parse_sass_value(value: &Value) -> Result<PropertyValue> {
|
|||
Ok(PropertyValue::Array(result))
|
||||
}
|
||||
Value::Color(_, Some(name)) => Ok(PropertyValue::String(name.clone())),
|
||||
Value::Numeric(n, _) => Ok(PropertyValue::String(format!("{}", n))),
|
||||
Value::Numeric(n, _) => Ok(PropertyValue::Number(n.to_integer())),
|
||||
Value::True => Ok(PropertyValue::String("true".to_string())),
|
||||
Value::False => Ok(PropertyValue::String("false".to_string())),
|
||||
_ => Err(Error(format!(
|
||||
|
|
@ -744,23 +756,22 @@ fn get_sass_string(value: &Value) -> Option<&str> {
|
|||
|
||||
fn resolve_path(base: &Path, p: &str) -> Result<PathBuf> {
|
||||
let path = Path::new(p);
|
||||
let mut result = base.to_owned();
|
||||
result.pop();
|
||||
let mut base = base.to_owned();
|
||||
base.pop();
|
||||
if path.starts_with(".") {
|
||||
result.push(path);
|
||||
if result.exists() {
|
||||
return Ok(result);
|
||||
base.push(path);
|
||||
if base.exists() {
|
||||
return Ok(base);
|
||||
}
|
||||
} else {
|
||||
loop {
|
||||
let mut result = base.clone();
|
||||
result.push("node_modules");
|
||||
result.push(path);
|
||||
if result.exists() {
|
||||
return Ok(result);
|
||||
}
|
||||
result.pop();
|
||||
result.pop();
|
||||
if !result.pop() {
|
||||
if !base.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -795,9 +806,10 @@ fn interpolation_error() -> Error {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use regex::Regex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_properties_immediate_child_and_descendant_selectors() {
|
||||
fn test_property_sheet_with_immediate_child_and_descendant_selectors() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -829,71 +841,71 @@ mod tests {
|
|||
// f1 single-element selector
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1"]),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2", "f1"]),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2", "f3", "f1"]),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
|
||||
// f2 single-element selector
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2"]),
|
||||
props(&[("color", "indigo"), ("height", "2")])
|
||||
props(&[("color", string("indigo")), ("height", num(2))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2", "f2"]),
|
||||
props(&[("color", "indigo"), ("height", "2")])
|
||||
props(&[("color", string("indigo")), ("height", num(2))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f3", "f2"]),
|
||||
props(&[("color", "indigo"), ("height", "2")])
|
||||
props(&[("color", string("indigo")), ("height", num(2))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f6", "f2"]),
|
||||
props(&[("color", "indigo"), ("height", "2")])
|
||||
props(&[("color", string("indigo")), ("height", num(2))])
|
||||
);
|
||||
|
||||
// f3 single-element selector
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f3"]),
|
||||
props(&[("color", "violet"), ("height", "3")])
|
||||
props(&[("color", string("violet")), ("height", num(3))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2", "f3"]),
|
||||
props(&[("color", "violet"), ("height", "3")])
|
||||
props(&[("color", string("violet")), ("height", num(3))])
|
||||
);
|
||||
|
||||
// f2 child selector
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f2"]),
|
||||
props(&[("color", "green"), ("height", "2")])
|
||||
props(&[("color", string("green")), ("height", num(2))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f2", "f1", "f2"]),
|
||||
props(&[("color", "green"), ("height", "2")])
|
||||
props(&[("color", string("green")), ("height", num(2))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f3", "f1", "f2"]),
|
||||
props(&[("color", "green"), ("height", "2")])
|
||||
props(&[("color", string("green")), ("height", num(2))])
|
||||
);
|
||||
|
||||
// f3 descendant selector
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f3"]),
|
||||
props(&[("color", "blue"), ("height", "3")])
|
||||
props(&[("color", string("blue")), ("height", num(3))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f2", "f3"]),
|
||||
props(&[("color", "blue"), ("height", "3")])
|
||||
props(&[("color", string("blue")), ("height", num(3))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query_simple(&sheet, vec!["f1", "f6", "f7", "f8", "f3"]),
|
||||
props(&[("color", "blue"), ("height", "3")])
|
||||
props(&[("color", string("blue")), ("height", num(3))])
|
||||
);
|
||||
|
||||
// no match
|
||||
|
|
@ -902,7 +914,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_properties_text_attribute() {
|
||||
fn test_property_sheet_with_text_attribute() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -927,15 +939,15 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("f1", None, true, 0)], "abc"),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("f1", None, true, 0)], "Abc"),
|
||||
props(&[("color", "green")])
|
||||
props(&[("color", string("green"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("f1", None, true, 0)], "AB_CD"),
|
||||
props(&[("color", "blue")])
|
||||
props(&[("color", string("blue"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("f2", None, true, 0)], "Abc"),
|
||||
|
|
@ -943,12 +955,12 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("f2", None, true, 0)], "ABC"),
|
||||
props(&[("color", "purple")])
|
||||
props(&[("color", string("purple"))])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_properties_with_fields() {
|
||||
fn test_property_sheet_with_fields() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -971,11 +983,11 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("a", None, true, 0)], ""),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("a", Some("x"), true, 0)], ""),
|
||||
props(&[("color", "green")])
|
||||
props(&[("color", string("green"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -983,7 +995,7 @@ mod tests {
|
|||
vec![("a", Some("x"), true, 0), ("b", None, true, 0)],
|
||||
""
|
||||
),
|
||||
props(&[("color", "blue")])
|
||||
props(&[("color", string("blue"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -991,15 +1003,15 @@ mod tests {
|
|||
vec![("a", Some("x"), true, 0), ("b", Some("y"), true, 0)],
|
||||
""
|
||||
),
|
||||
props(&[("color", "yellow")])
|
||||
props(&[("color", string("yellow"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("b", Some("x"), true, 0)], ""),
|
||||
props(&[("color", "violet")])
|
||||
props(&[("color", string("violet"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(&sheet, vec![("a", None, true, 0), ("b", None, true, 0)], ""),
|
||||
props(&[("color", "orange")])
|
||||
props(&[("color", string("orange"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -1007,12 +1019,12 @@ mod tests {
|
|||
vec![("a", None, true, 0), ("b", Some("y"), true, 0)],
|
||||
""
|
||||
),
|
||||
props(&[("color", "indigo")])
|
||||
props(&[("color", string("indigo"))])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_properties_cascade_ordering_as_tie_breaker() {
|
||||
fn test_property_sheet_with_cascade_ordering_as_tie_breaker() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -1038,7 +1050,7 @@ mod tests {
|
|||
vec![("f1", None, true, 0), ("f2", None, true, 1)],
|
||||
"x"
|
||||
),
|
||||
props(&[("color", "red")])
|
||||
props(&[("color", string("red"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -1046,7 +1058,7 @@ mod tests {
|
|||
vec![("f1", None, true, 1), ("f2", None, true, 1)],
|
||||
"x"
|
||||
),
|
||||
props(&[("color", "green")])
|
||||
props(&[("color", string("green"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -1054,7 +1066,7 @@ mod tests {
|
|||
vec![("f1", None, true, 1), ("f2", None, true, 1)],
|
||||
"a"
|
||||
),
|
||||
props(&[("color", "blue")])
|
||||
props(&[("color", string("blue"))])
|
||||
);
|
||||
assert_eq!(
|
||||
*query(
|
||||
|
|
@ -1062,12 +1074,12 @@ mod tests {
|
|||
vec![("f1", None, true, 1), ("f2", None, true, 1)],
|
||||
"ab"
|
||||
),
|
||||
props(&[("color", "violet")])
|
||||
props(&[("color", string("violet"))])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_properties_css_function_calls() {
|
||||
fn test_property_sheet_with_css_function_calls() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -1096,7 +1108,7 @@ mod tests {
|
|||
object(&[("name", string("g")), ("args", array(vec![string("h"),]))]),
|
||||
string("i"),
|
||||
string("j"),
|
||||
string("10"),
|
||||
num(10),
|
||||
])
|
||||
),
|
||||
])
|
||||
|
|
@ -1104,7 +1116,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_properties_array_by_declaring_property_multiple_times() {
|
||||
fn test_property_sheet_with_array_by_declaring_property_multiple_times() {
|
||||
let sheet = generate_property_sheet(
|
||||
"foo.css",
|
||||
"
|
||||
|
|
@ -1144,6 +1156,62 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_property_sheet_with_imports() {
|
||||
let repo_dir = TempDir::new().unwrap();
|
||||
let properties_dir = repo_dir.path().join("properties");
|
||||
let dependency_properties_dir = repo_dir
|
||||
.path()
|
||||
.join("node_modules")
|
||||
.join("the-dependency")
|
||||
.join("properties");
|
||||
fs::create_dir_all(&properties_dir).unwrap();
|
||||
fs::create_dir_all(&dependency_properties_dir).unwrap();
|
||||
let sheet_path1 = properties_dir.join("sheet1.css");
|
||||
let sheet_path2 = properties_dir.join("sheet2.css");
|
||||
let dependency_sheet_path1 = dependency_properties_dir.join("dependency-sheet1.css");
|
||||
let dependency_sheet_path2 = dependency_properties_dir.join("dependency-sheet2.css");
|
||||
|
||||
fs::write(
|
||||
sheet_path2,
|
||||
r#"
|
||||
a { x: '1'; }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dependency_sheet_path1,
|
||||
r#"
|
||||
@import "./dependency-sheet2.css";
|
||||
a { y: '2'; }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dependency_sheet_path2,
|
||||
r#"
|
||||
b { x: '3'; }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let sheet = generate_property_sheet(
|
||||
sheet_path1,
|
||||
r#"
|
||||
@import "./sheet2.css";
|
||||
@import "the-dependency/properties/dependency-sheet1.css";
|
||||
b { y: '4'; }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let a = query_simple(&sheet, vec!["a"]);
|
||||
assert_eq!(a["x"], string("1"),);
|
||||
assert_eq!(a["y"], string("2"),);
|
||||
let b = query_simple(&sheet, vec!["b"]);
|
||||
assert_eq!(b["x"], string("3"),);
|
||||
assert_eq!(b["y"], string("4"),);
|
||||
}
|
||||
|
||||
fn query_simple<'a>(
|
||||
sheet: &'a PropertySheetJSON,
|
||||
node_stack: Vec<&'static str>,
|
||||
|
|
@ -1197,9 +1265,13 @@ mod tests {
|
|||
PropertyValue::String(s.to_string())
|
||||
}
|
||||
|
||||
fn props<'a>(s: &'a [(&'a str, &'a str)]) -> PropertySet {
|
||||
fn num(n: isize) -> PropertyValue {
|
||||
PropertyValue::Number(n)
|
||||
}
|
||||
|
||||
fn props<'a>(s: &'a [(&'a str, PropertyValue)]) -> PropertySet {
|
||||
s.into_iter()
|
||||
.map(|(a, b)| (a.to_string(), PropertyValue::String(b.to_string())))
|
||||
.map(|(a, b)| (a.to_string(), b.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use crate::loader::Loader;
|
|||
use lazy_static::lazy_static;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tree_sitter::Language;
|
||||
use tree_sitter::{Language, PropertySheet};
|
||||
use tree_sitter_highlight::{load_property_sheet, Properties};
|
||||
|
||||
include!("./dirs.rs");
|
||||
|
||||
|
|
@ -20,6 +21,16 @@ pub fn get_language(name: &str) -> Language {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn get_property_sheet(language_name: &str, sheet_name: &str) -> PropertySheet<Properties> {
|
||||
let path = GRAMMARS_DIR
|
||||
.join(language_name)
|
||||
.join("src")
|
||||
.join(sheet_name);
|
||||
let json = fs::read_to_string(path).unwrap();
|
||||
let language = get_language(language_name);
|
||||
load_property_sheet(language, &json).unwrap()
|
||||
}
|
||||
|
||||
pub fn get_test_language(name: &str, parser_code: &str, path: Option<&Path>) -> Language {
|
||||
let parser_c_path = SCRATCH_DIR.join(&format!("{}-parser.c", name));
|
||||
if !fs::read_to_string(&parser_c_path)
|
||||
|
|
|
|||
214
cli/src/tests/highlight_test.rs
Normal file
214
cli/src/tests/highlight_test.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
use super::helpers::fixtures::{get_language, get_property_sheet};
|
||||
use lazy_static::lazy_static;
|
||||
use tree_sitter::{Language, PropertySheet};
|
||||
use tree_sitter_highlight::{highlight, highlight_html, HighlightEvent, Properties, Scope};
|
||||
|
||||
lazy_static! {
|
||||
static ref JS_SHEET: PropertySheet<Properties> =
|
||||
get_property_sheet("javascript", "highlights.json");
|
||||
static ref HTML_SHEET: PropertySheet<Properties> =
|
||||
get_property_sheet("html", "highlights.json");
|
||||
static ref SCOPE_CLASS_STRINGS: Vec<String> = {
|
||||
let mut result = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(scope) = Scope::from_usize(i) {
|
||||
result.push(format!("class={:?}", scope));
|
||||
i += 1;
|
||||
}
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_highlighting_injected_html_in_javascript() {
|
||||
let source = vec!["const s = html `<div>${a < b}</div>`;"].join("\n");
|
||||
|
||||
assert_eq!(
|
||||
&to_token_vector(&source, get_language("javascript"), &JS_SHEET).unwrap(),
|
||||
&[vec![
|
||||
("const", vec![Scope::Keyword]),
|
||||
(" ", vec![]),
|
||||
("s", vec![Scope::Variable]),
|
||||
(" ", vec![]),
|
||||
("=", vec![Scope::Operator]),
|
||||
(" ", vec![]),
|
||||
("html", vec![Scope::Function]),
|
||||
(" ", vec![]),
|
||||
("`<", vec![Scope::String]),
|
||||
("div", vec![Scope::String, Scope::Tag]),
|
||||
(">", vec![Scope::String]),
|
||||
(
|
||||
"${",
|
||||
vec![Scope::String, Scope::Embedded, Scope::PunctuationSpecial]
|
||||
),
|
||||
("a", vec![Scope::String, Scope::Embedded, Scope::Variable]),
|
||||
(" ", vec![Scope::String, Scope::Embedded]),
|
||||
("<", vec![Scope::String, Scope::Embedded, Scope::Operator]),
|
||||
(" ", vec![Scope::String, Scope::Embedded]),
|
||||
("b", vec![Scope::String, Scope::Embedded, Scope::Variable]),
|
||||
(
|
||||
"}",
|
||||
vec![Scope::String, Scope::Embedded, Scope::PunctuationSpecial]
|
||||
),
|
||||
("</", vec![Scope::String]),
|
||||
("div", vec![Scope::String, Scope::Tag]),
|
||||
(">`", vec![Scope::String]),
|
||||
(";", vec![Scope::PunctuationDelimiter]),
|
||||
]]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_highlighting_injected_javascript_in_html() {
|
||||
let source = vec![
|
||||
"<body>",
|
||||
" <script>",
|
||||
" const x = new Thing();",
|
||||
" </script>",
|
||||
"</body>",
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
assert_eq!(
|
||||
&to_token_vector(&source, get_language("html"), &HTML_SHEET).unwrap(),
|
||||
&[
|
||||
vec![("<", vec![]), ("body", vec![Scope::Tag]), (">", vec![]),],
|
||||
vec![(" <", vec![]), ("script", vec![Scope::Tag]), (">", vec![]),],
|
||||
vec![
|
||||
(" ", vec![]),
|
||||
("const", vec![Scope::Keyword]),
|
||||
(" ", vec![]),
|
||||
("x", vec![Scope::Variable]),
|
||||
(" ", vec![]),
|
||||
("=", vec![Scope::Operator]),
|
||||
(" ", vec![]),
|
||||
("new", vec![Scope::Keyword]),
|
||||
(" ", vec![]),
|
||||
("Thing", vec![Scope::Constructor]),
|
||||
("(", vec![Scope::PunctuationBracket]),
|
||||
(")", vec![Scope::PunctuationBracket]),
|
||||
(";", vec![Scope::PunctuationDelimiter]),
|
||||
],
|
||||
vec![
|
||||
(" </", vec![]),
|
||||
("script", vec![Scope::Tag]),
|
||||
(">", vec![]),
|
||||
],
|
||||
vec![("</", vec![]), ("body", vec![Scope::Tag]), (">", vec![]),],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_highlighting_multiline_scopes_to_html() {
|
||||
let source = vec![
|
||||
"const SOMETHING = `",
|
||||
" one ${",
|
||||
" two()",
|
||||
" } three",
|
||||
"`",
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
assert_eq!(
|
||||
&to_html(&source, get_language("javascript"), &JS_SHEET,).unwrap(),
|
||||
&[
|
||||
"<span class=Keyword>const</span> <span class=Constant>SOMETHING</span> <span class=Operator>=</span> <span class=String>`</span>\n".to_string(),
|
||||
"<span class=String> one <span class=Embedded><span class=PunctuationSpecial>${</span></span></span>\n".to_string(),
|
||||
"<span class=String><span class=Embedded> <span class=Function>two</span><span class=PunctuationBracket>(</span><span class=PunctuationBracket>)</span></span></span>\n".to_string(),
|
||||
"<span class=String><span class=Embedded> <span class=PunctuationSpecial>}</span></span> three</span>\n".to_string(),
|
||||
"<span class=String>`</span>\n".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_highlighting_empty_lines() {
|
||||
let source = vec![
|
||||
"class A {",
|
||||
"",
|
||||
" b(c) {",
|
||||
"",
|
||||
" d(e)",
|
||||
"",
|
||||
" }",
|
||||
"",
|
||||
"}",
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
assert_eq!(
|
||||
&to_html(&source, get_language("javascript"), &JS_SHEET,).unwrap(),
|
||||
&[
|
||||
"<span class=Keyword>class</span> <span class=Constructor>A</span> <span class=PunctuationBracket>{</span>\n".to_string(),
|
||||
"\n".to_string(),
|
||||
" <span class=Function>b</span><span class=PunctuationBracket>(</span><span class=Variable>c</span><span class=PunctuationBracket>)</span> <span class=PunctuationBracket>{</span>\n".to_string(),
|
||||
"\n".to_string(),
|
||||
" <span class=Function>d</span><span class=PunctuationBracket>(</span><span class=Variable>e</span><span class=PunctuationBracket>)</span>\n".to_string(),
|
||||
"\n".to_string(),
|
||||
" <span class=PunctuationBracket>}</span>\n".to_string(),
|
||||
"\n".to_string(),
|
||||
"<span class=PunctuationBracket>}</span>\n".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn test_language_for_injection_string<'a>(
|
||||
string: &str,
|
||||
) -> Option<(Language, &'a PropertySheet<Properties>)> {
|
||||
match string {
|
||||
"javascript" => Some((get_language("javascript"), &JS_SHEET)),
|
||||
"html" => Some((get_language("html"), &HTML_SHEET)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html<'a>(
|
||||
src: &'a str,
|
||||
language: Language,
|
||||
property_sheet: &'a PropertySheet<Properties>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
highlight_html(
|
||||
src.as_bytes(),
|
||||
language,
|
||||
property_sheet,
|
||||
&test_language_for_injection_string,
|
||||
&|scope| SCOPE_CLASS_STRINGS[scope as usize].as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_token_vector<'a>(
|
||||
src: &'a str,
|
||||
language: Language,
|
||||
property_sheet: &'a PropertySheet<Properties>,
|
||||
) -> Result<Vec<Vec<(&'a str, Vec<Scope>)>>, String> {
|
||||
let mut lines = Vec::new();
|
||||
let mut scopes = Vec::new();
|
||||
let mut line = Vec::new();
|
||||
for event in highlight(
|
||||
src.as_bytes(),
|
||||
language,
|
||||
property_sheet,
|
||||
&test_language_for_injection_string,
|
||||
)? {
|
||||
match event {
|
||||
HighlightEvent::ScopeStart(s) => scopes.push(s),
|
||||
HighlightEvent::ScopeEnd => {
|
||||
scopes.pop();
|
||||
}
|
||||
HighlightEvent::Source(s) => {
|
||||
for (i, l) in s.lines().enumerate() {
|
||||
if i > 0 {
|
||||
lines.push(line);
|
||||
line = Vec::new();
|
||||
}
|
||||
if l.len() > 0 {
|
||||
line.push((l, scopes.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push(line);
|
||||
Ok(lines)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod corpus_test;
|
||||
mod helpers;
|
||||
mod highlight_test;
|
||||
mod node_test;
|
||||
mod parser_test;
|
||||
mod properties_test;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue