Skip to content

Commit 290a08a

Browse files
committed
Merge remote-tracking branch 'upstream/master' into typst-support
2 parents 79e743e + a697b29 commit 290a08a

File tree

17 files changed

+451
-38
lines changed

17 files changed

+451
-38
lines changed

Cargo.lock

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
[workspace]
2-
members = [ "harper-cli", "harper-core", "harper-ls", "harper-comments", "harper-wasm", "harper-tree-sitter", "harper-html", "harper-typst"]
2+
members = [ "harper-cli", "harper-core", "harper-ls", "harper-comments", "harper-wasm", "harper-tree-sitter", "harper-html", "harper-literate-haskell", "harper-typst" ]
33
resolver = "2"
44

55
[profile.release]
6+
codegen-units = 1
7+
lto = true
68
opt-level = 3
79
strip = true

harper-cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository = "https://github.com/automattic/harper"
1010
anyhow = "1.0.95"
1111
ariadne = "0.4.1"
1212
clap = { version = "4.5.23", features = ["derive"] }
13+
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.15.0" }
1314
harper-core = { path = "../harper-core", version = "0.15.0" }
1415
harper-comments = { path = "../harper-comments", version = "0.15.0" }
1516
harper-typst = { path = "../harper-typst", version = "0.15.0" }

harper-cli/src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use harper_comments::CommentParser;
99
use harper_core::linting::{LintGroup, LintGroupConfig, Linter};
1010
use harper_core::parsers::Markdown;
1111
use harper_core::{remove_overlaps, Dictionary, Document, FstDictionary, TokenKind};
12+
use harper_literate_haskell::LiterateHaskellParser;
1213

1314
#[derive(Debug, Parser)]
1415
enum Args {
@@ -171,6 +172,7 @@ fn load_file(file: &Path) -> anyhow::Result<(Document, String)> {
171172
let parser: Box<dyn harper_core::parsers::Parser> =
172173
match file.extension().map(|v| v.to_str().unwrap()) {
173174
Some("md") => Box::new(Markdown),
175+
Some("lhs") => Box::new(LiterateHaskellParser),
174176
Some("typ") => Box::new(harper_typst::Typst),
175177
_ => Box::new(
176178
CommentParser::new_from_filename(file)

harper-literate-haskell/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "harper-literate-haskell"
3+
version = "0.15.0"
4+
edition = "2021"
5+
description = "The language checker for developers."
6+
license = "Apache-2.0"
7+
repository = "https://github.com/automattic/harper"
8+
9+
[dependencies]
10+
harper-core = { path = "../harper-core", version = "0.15.0" }
11+
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.15.0" }
12+
harper-comments = { path = "../harper-comments", version = "0.15.0" }
13+
itertools = "0.13.0"
14+
paste = "1.0.14"

harper-literate-haskell/src/lib.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use harper_comments::CommentParser;
2+
use harper_core::{
3+
parsers::{Markdown, Mask, Parser},
4+
FullDictionary, Masker, Token,
5+
};
6+
7+
mod masker;
8+
use itertools::Itertools;
9+
use masker::LiterateHaskellMasker;
10+
11+
/// Parses a Literate Haskell document by masking out the code and considering text as Markdown.
12+
pub struct LiterateHaskellParser;
13+
14+
impl LiterateHaskellParser {
15+
pub fn create_ident_dict(&self, source: &[char]) -> Option<FullDictionary> {
16+
let parser = CommentParser::new_from_language_id("haskell").unwrap();
17+
let mask = LiterateHaskellMasker::code_only().create_mask(source);
18+
19+
let code = mask
20+
.iter_allowed(source)
21+
.flat_map(|(_, src)| src.to_owned())
22+
.collect_vec();
23+
parser.create_ident_dict(&code)
24+
}
25+
}
26+
27+
impl Parser for LiterateHaskellParser {
28+
fn parse(&self, source: &[char]) -> Vec<Token> {
29+
Mask::new(LiterateHaskellMasker::text_only(), Markdown).parse(source)
30+
}
31+
}

harper-literate-haskell/src/masker.rs

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use harper_core::{CharStringExt, Mask, Masker, Span};
2+
3+
/// Masker for selecting portions of Literate Haskell documents.
4+
///
5+
/// Based on the specifications outlined at https://wiki.haskell.org/Literate_programming.
6+
pub struct LiterateHaskellMasker {
7+
text: bool,
8+
code: bool,
9+
}
10+
11+
impl LiterateHaskellMasker {
12+
pub fn text_only() -> Self {
13+
Self {
14+
text: true,
15+
code: false,
16+
}
17+
}
18+
19+
pub fn code_only() -> Self {
20+
Self {
21+
text: false,
22+
code: true,
23+
}
24+
}
25+
}
26+
27+
impl Masker for LiterateHaskellMasker {
28+
fn create_mask(&self, source: &[char]) -> harper_core::Mask {
29+
let mut mask = Mask::new_blank();
30+
31+
let mut location = 0;
32+
let mut in_code_env = false;
33+
let mut last_line_blank = false;
34+
35+
for line in source.split(|c| *c == '\n') {
36+
let string_form = line.to_string();
37+
let trimmed = string_form.trim();
38+
let line_is_bird = line.first().is_some_and(|c| *c == '>');
39+
40+
// Code fencing
41+
let latex_style = matches!(trimmed, r"\begin{code}" | r"\end{code}");
42+
let code_start = trimmed == r"\begin{code}" || (last_line_blank && line_is_bird);
43+
let code_end = trimmed == r"\end{code}" || trimmed.is_empty();
44+
45+
// Toggle on fence
46+
if (!in_code_env && code_start) || (in_code_env && code_end) {
47+
in_code_env = !in_code_env;
48+
49+
// Exclude latex-style fence
50+
if latex_style {
51+
location += line.len() + 1; // +1 for the newline split on
52+
last_line_blank = trimmed.is_empty();
53+
continue;
54+
}
55+
56+
// Exclude newline after code for bird style
57+
if trimmed.is_empty() {
58+
location += line.len() + 1; // +1 for the newline split on
59+
last_line_blank = true;
60+
continue;
61+
}
62+
}
63+
64+
let end_loc = location + line.len();
65+
if (!in_code_env && self.text) || (in_code_env && self.code) {
66+
let start_loc = if line_is_bird { location + 2 } else { location };
67+
mask.push_allowed(Span::new(start_loc, end_loc));
68+
}
69+
70+
location = end_loc + 1; // +1 for the newline split on
71+
last_line_blank = trimmed.is_empty();
72+
}
73+
74+
mask.merge_whitespace_sep(source);
75+
mask
76+
}
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use harper_core::{Masker, Span};
82+
use itertools::Itertools;
83+
84+
use super::LiterateHaskellMasker;
85+
86+
#[test]
87+
fn bird_format() {
88+
let source = r"Text here
89+
90+
> fact :: Integer -> Integer
91+
> fact 0 = 1
92+
> fact n = n * fact (n-1)
93+
94+
Text here
95+
"
96+
.chars()
97+
.collect_vec();
98+
99+
let text_mask = LiterateHaskellMasker::text_only().create_mask(&source);
100+
assert_eq!(
101+
text_mask
102+
.iter_allowed(&source)
103+
.map(|(s, _)| s)
104+
.collect_vec(),
105+
vec![Span::new(0, 10), Span::new(80, 90)],
106+
);
107+
108+
let code_mask = LiterateHaskellMasker::code_only().create_mask(&source);
109+
assert_eq!(
110+
code_mask
111+
.iter_allowed(&source)
112+
.map(|(s, _)| s)
113+
.collect_vec(),
114+
vec![Span::new(13, 39), Span::new(42, 52), Span::new(55, 78)],
115+
);
116+
}
117+
118+
#[test]
119+
fn latex_format() {
120+
let source = r#"Text here
121+
\begin{code}
122+
main :: IO ()
123+
main = print "just an example"
124+
\end{code}
125+
Text here
126+
"#
127+
.chars()
128+
.collect_vec();
129+
130+
let text_mask = LiterateHaskellMasker::text_only().create_mask(&source);
131+
assert_eq!(
132+
text_mask
133+
.iter_allowed(&source)
134+
.map(|(s, _)| s)
135+
.collect_vec(),
136+
vec![Span::new(0, 9), Span::new(79, 89)],
137+
);
138+
139+
let code_mask = LiterateHaskellMasker::code_only().create_mask(&source);
140+
assert_eq!(
141+
code_mask
142+
.iter_allowed(&source)
143+
.map(|(s, _)| s)
144+
.collect_vec(),
145+
vec![Span::new(23, 67)],
146+
);
147+
}
148+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use harper_core::linting::{LintGroup, LintGroupConfig, Linter};
2+
use harper_core::{Document, FstDictionary};
3+
use harper_literate_haskell::LiterateHaskellParser;
4+
5+
/// Creates a unit test checking that the linting of a Markdown document (in
6+
/// `tests_sources`) produces the expected number of lints.
7+
macro_rules! create_test {
8+
($filename:ident.lhs, $correct_expected:expr) => {
9+
paste::paste! {
10+
#[test]
11+
fn [<lints_ $filename _correctly>](){
12+
let source = include_str!(
13+
concat!(
14+
"./test_sources/",
15+
concat!(stringify!($filename), ".lhs")
16+
)
17+
);
18+
19+
let dict = FstDictionary::curated();
20+
let document = Document::new_curated(&source, &mut LiterateHaskellParser);
21+
22+
let mut linter = LintGroup::new(
23+
LintGroupConfig::default(),
24+
dict
25+
);
26+
let lints = linter.lint(&document);
27+
28+
dbg!(&lints);
29+
assert_eq!(lints.len(), $correct_expected);
30+
31+
// Make sure that all generated tokens span real characters
32+
for token in document.tokens(){
33+
assert!(token.span.try_get_content(document.get_source()).is_some());
34+
}
35+
}
36+
}
37+
};
38+
}
39+
40+
create_test!(bird_format.lhs, 2);
41+
create_test!(latex_format.lhs, 2);
42+
create_test!(mixed_format.lhs, 4);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Sourced from https://wiki.haskell.org/Literate_programming.
2+
3+
In Bird-style you have to leave a blnk before the code.
4+
5+
> fact :: Integer -> Integer
6+
> fact 0 = 1
7+
> fact n = n * fact (n-1)
8+
9+
And you have to leave a blnk line after the code as well.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Sourced from https://wiki.haskell.org/Literate_programming.
2+
3+
And the definition of the following function
4+
would totally screw up my program, so I'm not
5+
definining it:
6+
7+
\begin{code}
8+
main :: IO ()
9+
main = print "just an example"
10+
\end{code}
11+
12+
Seee?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Sourced from https://wiki.haskell.org/Literate_programming.
2+
3+
In Bird-style you have to leave a blnk before the code.
4+
5+
> fact :: Integer -> Integer
6+
> fact 0 = 1
7+
> fact n = n * fact (n-1)
8+
9+
And you have to leave a blnk line after the code as well.
10+
11+
And the definition of the following function
12+
would totally screw up my program, so I'm not
13+
definining it:
14+
15+
\begin{code}
16+
main :: IO ()
17+
main = print "just an example"
18+
\end{code}
19+
20+
Seee?

harper-ls/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ readme = "README.md"
88
repository = "https://github.com/automattic/harper"
99

1010
[dependencies]
11+
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.15.0" }
1112
harper-core = { path = "../harper-core", version = "0.15.0", features = ["concurrent"] }
1213
harper-comments = { path = "../harper-comments", version = "0.15.0" }
1314
harper-typst = { path = "../harper-typst", version = "0.15.0" }

0 commit comments

Comments
 (0)