feat: add ink support (#1894)

* feat: add harper_ink crate with impl and tests

* feat: add harper_ink to harper_ls

* docs: add `ink` to the list of supported languages

---------

Co-authored-by: Elijah Potter <me@elijahpotter.dev>
This commit is contained in:
Freddie Gilbraith 2025-09-26 16:10:40 +01:00 committed by GitHub
parent 8a70efa51c
commit 690100cfb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 157 additions and 1 deletions

16
harper-ink/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "harper-ink"
version = "0.65.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.65.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.65.0" }
tree-sitter-ink-lbz = "0.0.1"
tree-sitter = "0.25.8"
[dev-dependencies]
paste = "1.0.15"

39
harper-ink/src/lib.rs Normal file
View file

@ -0,0 +1,39 @@
use harper_core::parsers::{self, Parser, PlainEnglish};
use harper_core::{Token, TokenKind};
use harper_tree_sitter::TreeSitterMasker;
use tree_sitter::Node;
pub struct InkParser {
inner: parsers::Mask<TreeSitterMasker, PlainEnglish>,
}
impl InkParser {
fn node_condition(n: &Node) -> bool {
matches!(n.kind(), "contentText" | "blockComment" | "lineComment")
}
}
impl Default for InkParser {
fn default() -> Self {
Self {
inner: parsers::Mask::new(
TreeSitterMasker::new(tree_sitter_ink_lbz::LANGUAGE.into(), Self::node_condition),
PlainEnglish,
),
}
}
}
impl Parser for InkParser {
fn parse(&self, source: &[char]) -> Vec<Token> {
let mut tokens = self.inner.parse(source);
for token in &mut tokens {
if let TokenKind::Space(v) = &mut token.kind {
*v = (*v).clamp(0, 1);
}
}
tokens
}
}

View file

@ -0,0 +1,41 @@
use harper_core::linting::{LintGroup, Linter};
use harper_core::spell::FstDictionary;
use harper_core::{Dialect, Document};
use harper_ink::InkParser;
/// Creates a unit test checking that the linting of a Ink document (in
/// `tests_sources`) produces the expected number of lints.
macro_rules! create_test {
($filename:ident.ink, $correct_expected:expr) => {
paste::paste! {
#[test]
fn [<lints_ $filename _correctly>](){
let source = include_str!(
concat!(
"./test_sources/",
concat!(stringify!($filename), ".ink")
)
);
let dict = FstDictionary::curated();
let document = Document::new(&source, &InkParser::default(),
&FstDictionary::curated()
);
let mut linter = LintGroup::new_curated(dict, Dialect::American);
let lints = linter.lint(&document);
dbg!(&lints);
assert_eq!(lints.len(), $correct_expected);
// Make sure that all generated tokens span real characters
for token in document.tokens(){
assert!(token.span.try_get_content(document.get_source()).is_some());
}
}
}
};
}
create_test!(good.ink, 0);
create_test!(bad.ink, 6);

View file

@ -0,0 +1,15 @@
=== Knot ===
text here is checked: chungus
= Stitch
~identifiersAreNotchecked = "but strings are: chungus"
// comments are also checked -> chungus
/*
chungus
*/
* choices are checked
+ chungus
+ normal text

View file

@ -0,0 +1,15 @@
=== Knot ===
this is a thing
= Stitch
* an option
+ another option
+ normal text
+ + indented choice
+ + another indented choice
- gather
= ThreeWordStitch
test