Skip to content

Commit

Permalink
Replace Reference with Footnote
Browse files Browse the repository at this point in the history
A Footnote is a Reference that can wrap across multiple lines. This
helps (minimally) in projects like Git and Linux but will not work for
anyone that uses "Markdown" footnotes since those implementations seem
never to support multiline footnotes.
  • Loading branch information
commonquail committed Sep 26, 2018
1 parent 16fff21 commit 6155000
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 48 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ understanding of patterns often seen in commit messages.
place, and breaking is actively detrimental to URLs. This change alone yields
a 6-8 times speed-up as reported by `time(1)`.

- _References_ may span multiple lines, subsequent lines following the same
indentation rules as _list items_. The syntactical unit has been renamed to
_footnote_ to better capture the new functionality.

## 1.1.0 - 2018-08-25

- #3: If the `core.commentChar` setting is set to an explicit character, use
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ In summary, `commitmsgfmt`
- properly indents continuation lines in numbered and unnumbered lists,
recognizing several different list styles;

- exempts comments; text indented at least 4 spaces or 1 tab; "trailers"
(`Signed-off-by:`); and IEEE-style references from reformatting;
- exempts comments; text indented at least 4 spaces or 1 tab; and "trailers"
(`Signed-off-by:`);

- assumes UTF-8 encoded input but can gracefully degrade to ISO-8859-1
("latin1") which has been observed in the Linux kernel;
Expand Down
16 changes: 9 additions & 7 deletions doc/commitmsgfmt.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,11 @@ not a hard rule, and {self} tries not be a stick.

Except for the subject line, items can appear in any order.

=== Reference
=== Footnotes

A line that starts with a left bracket (*[*) followed by one or more
non-whitespace characters, a right bracket (*]*), an optional colon (*:*), and
a space is considered a _reference_, similar in style to the one IEEE
whitepapers often use:
a space is considered a _footnote_:

----
Official Git Web site [1][git]
Expand All @@ -114,10 +113,13 @@ Official Git Web site [1][git]
[git]: https://git-scm.com/
----

References are not wrapped, on account of frequently being URLs. References are
not considered to have a natural order and correspondingly are not sorted, and
the identifier between the brackets is not cross-checked with the rest of the
message.
This syntax is inspired by the IEEE whitepaper reference style.

Footnotes are wrapped like _paragraphs_, with continuation lines space-indented
to match the first line. Additional spaces after the mandatory space are
stripped. Footnotes are not considered to have a natural order and
correspondingly are not sorted, and the identifier between the brackets is not
cross-checked with the rest of the message.

=== Trailer

Expand Down
40 changes: 32 additions & 8 deletions src/commitmsgfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ impl CommitMsgFmt {
self.wrap_paragraph_into(&mut buf, &p, None);
buf.push('\n');
}
Reference(ref s) | Subject(ref s) | Trailer(ref s) => {
Footnote(ref key, ref rest) => {
buf.push_str(&key);
buf.push(' ');
let continuation = " ".repeat(key.graphemes(true).count() + 1);
self.wrap_paragraph_into(&mut buf, &rest.trim(), Some(&continuation));
buf.push('\n');
}
Subject(ref s) | Trailer(ref s) => {
buf.push_str(s.as_str());
buf.push('\n');
}
Expand Down Expand Up @@ -177,20 +184,37 @@ foo
}

#[test]
fn formats_references() {
// References don't wrap. Many are just URLs, which don't wrap nicely,
// or short sentences that don't qualify for wrapping, but some
// references are entire paragraphs of prose and ought to be wrapped.
// See: git -C ../git/ log --format=%b | grep '^\[[^]]\+\] '
fn formats_footnotes() {
let msg = "
foo
[2] note
[1] note
[reference] reference extending beyond line-wrapping limit
[3] foo bar baz qux https://a.really-long-url.example
[4] https://a.really-long-url.example
[footnote] footnote extending
beyond line-wrapping
limit
[ä] multi-code-point footnote key
";
let expected = "
foo
[2] note
[1] note
[3] foo bar baz qux
https://a.really-long-url.example
[4] https://a.really-long-url.example
[footnote] footnote
extending
beyond
line-wrapping
limit
[ä] multi-code-point
footnote key
";

assert_eq!(filter(10, &msg), msg, "print references literally");
assert_eq!(filter(20, &msg), expected);
}

#[test]
Expand Down
77 changes: 46 additions & 31 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct ListIndent(pub String);
#[derive(Debug, PartialEq)]
pub enum Token {
Comment(String),
Reference(String),
Footnote(String, String),
ListItem(ListIndent, ListType, String),
Literal(String),
Paragraph(String),
Expand All @@ -27,7 +27,7 @@ pub fn parse(input: &str, comment_char: char) -> Vec<Token> {
let mut has_scissors = false;
let lines = input.lines();
let blank_or_empty = Regex::new(r"^\s*$").unwrap();
let reference = Regex::new(r"^\[[^]]+\]:? .+$").unwrap();
let footnote = Regex::new(r"^\[[^]]+\]:? .+$").unwrap();
let trailer = Regex::new(r"^\p{Alphabetic}[-\w]+: .+$").unwrap();
let indented = Regex::new(r"^(?:\t| {4,})").unwrap();
let list_item = Regex::new(
Expand Down Expand Up @@ -81,11 +81,20 @@ pub fn parse(input: &str, comment_char: char) -> Vec<Token> {
} else if !has_subject {
parse_subject(line, &mut toks);
has_subject = true;
} else if reference.is_match(line) {
toks.push(Token::Reference(line.to_owned()));
} else if footnote.is_match(line) {
debug_assert!(footnote.as_str().contains(' '));
let mut splitter = line.splitn(2, ' ');
let key = splitter.next().unwrap().to_owned();
let rest = splitter.next().unwrap().trim().to_owned();
toks.push(Token::Footnote(key, rest));
} else if trailer.is_match(line) {
toks.push(Token::Trailer(line.to_owned()));
} else if let Some(y) = match toks.last_mut() {
Some(&mut Token::Footnote(_, ref mut b)) => {
b.push(' ');
b.push_str(line.trim());
None
}
Some(&mut Token::Paragraph(ref mut b)) => {
b.push(' ');
b.push_str(line.trim());
Expand Down Expand Up @@ -489,20 +498,21 @@ Signed-off-by: Jane Doe <[email protected]>
}

#[test]
fn references_are_left_bracket_ident_right_bracket_space_text() {
fn footnotes_are_left_bracket_ident_right_bracket_space_text() {
assert_eq!(
parse(
"
subject
[1] reference
[re-fe] rence
[1] footnote
[fo-ot] note
[ä] multi-code-point footnote key
[@]: reference
[@]: footnote
[] not a reference
[] not a footnote
[1]not a reference
[1]not a footnote
[1]
"
Expand All @@ -511,22 +521,26 @@ subject
VerticalSpace,
Subject("subject".to_owned()),
VerticalSpace,
Reference("[1] reference".to_owned()),
Reference("[re-fe] rence".to_owned()),
Footnote("[1]".to_owned(), "footnote".to_owned()),
Footnote("[fo-ot]".to_owned(), "note".to_owned()),
Footnote(
"[ä]".to_owned(),
"multi-code-point footnote key".to_owned()
),
VerticalSpace,
Reference("[@]: reference".to_owned()),
Footnote("[@]:".to_owned(), "footnote".to_owned()),
VerticalSpace,
Paragraph("[] not a reference".to_owned()),
Paragraph("[] not a footnote".to_owned()),
VerticalSpace,
Paragraph("[1]not a reference".to_owned()),
Paragraph("[1]not a footnote".to_owned()),
VerticalSpace,
Paragraph("[1]".to_owned()),
],
);
}

#[test]
fn reference_order_is_unchanged() {
fn footnote_order_is_unchanged() {
// Naive solution is technically trivial but may not match semantics.
assert_eq!(
parse(
Expand All @@ -543,42 +557,43 @@ subject
VerticalSpace,
Subject("subject".to_owned()),
VerticalSpace,
Reference("[2] bar".to_owned()),
Reference("[b] a".to_owned()),
Reference("[a] b".to_owned()),
Reference("[1] foo".to_owned()),
Footnote("[2]".to_owned(), "bar".to_owned()),
Footnote("[b]".to_owned(), "a".to_owned()),
Footnote("[a]".to_owned(), "b".to_owned()),
Footnote("[1]".to_owned(), "foo".to_owned()),
],
);
}

#[test]
fn bug_references_are_single_line_only() {
// Some references are prose and should be treated accordingly.
// Regrettably this clashes with references that should not wrap. Maybe
// fixable by identifying unwrappable words instead of unwrappable
// lines.
fn footnotes_may_span_multiple_lines() {
assert_eq!(
parse(
"
subject
[1] foo
bar
[2] foo
bar
[3] foo
bar
"
),
[
VerticalSpace,
Subject("subject".to_owned()),
VerticalSpace,
Reference("[1] foo".to_owned()),
Paragraph("bar".to_owned()),
Footnote("[1]".to_owned(), "foo bar".to_owned()),
Footnote("[2]".to_owned(), "foo bar".to_owned()),
Footnote("[3]".to_owned(), "foo bar".to_owned()),
],
);
}

#[test]
fn bug_reference_idents_are_not_disambiguated() {
// Nice-to-have but not really our job. Requires tracking all references
fn bug_footnote_idents_are_not_disambiguated() {
// Nice-to-have but not really our job. Requires tracking all footnotes.
assert_eq!(
parse(
"
Expand All @@ -592,8 +607,8 @@ subject
VerticalSpace,
Subject("subject".to_owned()),
VerticalSpace,
Reference("[1] foo".to_owned()),
Reference("[1] bar".to_owned()),
Footnote("[1]".to_owned(), "foo".to_owned()),
Footnote("[1]".to_owned(), "bar".to_owned()),
],
);
}
Expand Down

0 comments on commit 6155000

Please sign in to comment.