-
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Shahar Dawn Or <mightyiampresence@gmail.com>
- Loading branch information
Showing
19 changed files
with
869 additions
and
283 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
use rnix::ast::{self, AstToken}; | ||
use rnix::{match_ast, SyntaxNode}; | ||
use rowan::ast::AstNode; | ||
|
||
/// Implements functions for doc-comments according to rfc145. | ||
pub trait DocComment { | ||
fn doc_text(&self) -> Option<&str>; | ||
} | ||
|
||
impl DocComment for ast::Comment { | ||
/// Function returns the contents of the doc-comment, if the [ast::Comment] is a | ||
/// doc-comment, or None otherwise. | ||
/// | ||
/// Note: [ast::Comment] holds both the single-line and multiline comment. | ||
/// | ||
/// /**{content}*/ | ||
/// -> {content} | ||
/// | ||
/// It is named `doc_text` to complement [ast::Comment::text]. | ||
fn doc_text(&self) -> Option<&str> { | ||
let text = self.syntax().text(); | ||
// Check whether this is a doc-comment | ||
if text.starts_with(r#"/**"#) && self.text().starts_with('*') { | ||
self.text().strip_prefix('*') | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Function retrieves a doc-comment from the [ast::Expr] | ||
/// | ||
/// Returns an [Option<String>] of the first suitable doc-comment. | ||
/// Returns [None] in case no suitable comment was found. | ||
/// | ||
/// Doc-comments can appear in two places for any expression | ||
/// | ||
/// ```nix | ||
/// # (1) directly before the expression (anonymous) | ||
/// /** Doc */ | ||
/// bar: bar; | ||
/// | ||
/// # (2) when assigning a name. | ||
/// { | ||
/// /** Doc */ | ||
/// foo = bar: bar; | ||
/// } | ||
/// ``` | ||
/// | ||
/// If the doc-comment is not found in place (1) the search continues at place (2) | ||
/// More precisely before the NODE_ATTRPATH_VALUE (ast) | ||
/// If no doc-comment was found in place (1) or (2) this function returns None. | ||
pub fn get_expr_docs(expr: &SyntaxNode) -> Option<String> { | ||
if let Some(doc) = get_doc_comment(expr) { | ||
// Found in place (1) | ||
doc.doc_text().map(|v| v.to_owned()) | ||
} else if let Some(ref parent) = expr.parent() { | ||
match_ast! { | ||
match parent { | ||
ast::AttrpathValue(_) => { | ||
if let Some(doc_comment) = get_doc_comment(parent) { | ||
doc_comment.doc_text().map(|v| v.to_owned()) | ||
}else{ | ||
None | ||
} | ||
}, | ||
_ => { | ||
// Yet unhandled ast-nodes | ||
None | ||
} | ||
|
||
} | ||
} | ||
// None | ||
} else { | ||
// There is no parent; | ||
// No further places where a doc-comment could be. | ||
None | ||
} | ||
} | ||
|
||
/// Looks backwards from the given expression | ||
/// Only whitespace or non-doc-comments are allowed in between an expression and the doc-comment. | ||
/// Any other Node or Token stops the peek. | ||
fn get_doc_comment(expr: &SyntaxNode) -> Option<ast::Comment> { | ||
let mut prev = expr.prev_sibling_or_token(); | ||
loop { | ||
match prev { | ||
Some(rnix::NodeOrToken::Token(ref token)) => { | ||
match_ast! { match token { | ||
ast::Whitespace(_) => { | ||
prev = token.prev_sibling_or_token(); | ||
}, | ||
ast::Comment(it) => { | ||
if it.doc_text().is_some() { | ||
break Some(it); | ||
}else{ | ||
//Ignore non-doc comments. | ||
prev = token.prev_sibling_or_token(); | ||
} | ||
}, | ||
_ => { | ||
break None; | ||
} | ||
}} | ||
} | ||
_ => break None, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
use textwrap::dedent; | ||
|
||
/// Ensure all lines in a multi-line doc-comments have the same indentation. | ||
/// | ||
/// Consider such a doc comment: | ||
/// | ||
/// ```nix | ||
/// { | ||
/// /* foo is | ||
/// the value: | ||
/// 10 | ||
/// */ | ||
/// foo = 10; | ||
/// } | ||
/// ``` | ||
/// | ||
/// The parser turns this into: | ||
/// | ||
/// ``` | ||
/// foo is | ||
/// the value: | ||
/// 10 | ||
/// ``` | ||
/// | ||
/// | ||
/// where the first line has no leading indentation, and all other lines have preserved their | ||
/// original indentation. | ||
/// | ||
/// What we want instead is: | ||
/// | ||
/// ``` | ||
/// foo is | ||
/// the value: | ||
/// 10 | ||
/// ``` | ||
/// | ||
/// i.e. we want the whole thing to be dedented. To achieve this, we remove all leading whitespace | ||
/// from the first line, and remove all common whitespace from the rest of the string. | ||
pub fn handle_indentation(raw: &str) -> Option<String> { | ||
let result: String = match raw.split_once('\n') { | ||
Some((first, rest)) => { | ||
format!("{}\n{}", first.trim_start(), dedent(rest)) | ||
} | ||
None => raw.into(), | ||
}; | ||
|
||
Some(result.trim().to_owned()).filter(|s| !s.is_empty()) | ||
} | ||
|
||
/// Shift down markdown headings | ||
/// | ||
/// Performs a line-wise matching to ' # Heading ' | ||
/// | ||
/// Counts the current numbers of '#' and adds levels: [usize] to them | ||
/// levels := 1; gives | ||
/// '# Heading' -> '## Heading' | ||
/// | ||
/// Markdown has 6 levels of headings. Everything beyond that (e.g., H7) may produce unexpected renderings. | ||
/// by default this function makes sure, headings don't exceed the H6 boundary. | ||
/// levels := 2; | ||
/// ... | ||
/// H4 -> H6 | ||
/// H6 -> H6 | ||
/// | ||
pub fn shift_headings(raw: &str, levels: usize) -> String { | ||
let mut result = String::new(); | ||
for line in raw.lines() { | ||
if line.trim_start().starts_with('#') { | ||
result.push_str(&format!("{}\n", &handle_heading(line, levels))); | ||
} else { | ||
result.push_str(&format!("{line}\n")); | ||
} | ||
} | ||
result | ||
} | ||
|
||
// Dumb heading parser. | ||
pub fn handle_heading(line: &str, levels: usize) -> String { | ||
let chars = line.chars(); | ||
|
||
let mut leading_trivials: String = String::new(); | ||
let mut hashes = String::new(); | ||
let mut rest = String::new(); | ||
for char in chars { | ||
match char { | ||
' ' | '\t' if hashes.is_empty() => { | ||
// only collect trivial before the initial hash | ||
leading_trivials.push(char) | ||
} | ||
'#' if rest.is_empty() => { | ||
// only collect hashes if no other tokens | ||
hashes.push(char) | ||
} | ||
_ => rest.push(char), | ||
} | ||
} | ||
let new_hashes = match hashes.len() + levels { | ||
// We reached the maximum heading size. | ||
6.. => "#".repeat(6), | ||
_ => "#".repeat(hashes.len() + levels), | ||
}; | ||
|
||
format!("{leading_trivials}{new_hashes} {rest}") | ||
} |
Oops, something went wrong.