Repository: sindresorhus/awesome-lint
Pull request #101 (2020-04-28 13:01:15)

Check and enforce quotes with punctuations
This is an attempt to specify punctuation rules around quotes, as discussed in #96.

Progress:
* [x] Validate quoting specifications with @sindresorhus 
* [x] Complete inventory of valid and invalid cases
* [x] Implement test fixtures 
* [x] Write validation and invalidation logic in Python
* [x] Translate Python logic to JS
* [x] Fix Travis
* [x] Profit


sindresorhus: I agree with what's shown in the fixtures.
kdeldycke: Thanks @sindresorhus for validating my fixtures!

As I [told you in another comment, I'm no JS dev](https://github.com/sindresorhus/awesome-lint/pull/102#issuecomment-622915561) so I'll need some extra time to figure out the test ecosystem. But I'll be able to produce a nice logical flow and regexps to have the checks properly in place.
sindresorhus: @kdeldycke Friendly bump :)
kdeldycke: @sindresorhus fair enough! :) I still want to tackle this one for good.

But I'm currently down the rabbit hole trying to reconcile some Markdown edge-cases in other formatters and linters: https://twitter.com/kdeldycke/status/1278735173903945734 . `awesome-lint` is at the end of the Markdown food chain, so I'm extra-slow.
sindresorhus: Travis is failing: https://travis-ci.com/github/sindresorhus/awesome-lint/jobs/360749236
kdeldycke: I've made progress on that one. Again, I'm no JS dev. So I first tried to attack the issue with my language of choice. Here is a working logical implementation in Python:

```python
import re

valid = """
- [foo](https://foo.com) - Valid description.
- [foo](https://foo.com) - A valid description.
- [foo](https://foo.com) - A valid description...
- [foo](https://foo.com) - A valid description.......
- [foo](https://foo.com) - A valid description…
- [foo](https://foo.com) - A valid description..….....
- [foo](https://foo.com) - A valid description!
- [foo](https://foo.com) - A valid description! ⭐
- [foo](https://foo.com) - A valid description!!!
- [foo](https://foo.com) - A valid description?
- [foo](https://foo.com) - A valid description???
- [foo](https://foo.com) - A valid description????!??
- [foo](https://foo.com) - A valid description..…!?…....
- [foo](https://foo.com) - A valid description with [link](https://bar.org).
- [foo](https://foo.com) - A valid description. ![image](image.png)
- [foo](https://foo.com) - A valid description. <img src="image.png">
- [foo](https://foo.com) - `valid description` here.
- [foo](https://foo.com) - `valid` is a word that is `great`.
- [foo](https://foo.com) - `valid` is a word that is `great`!?!.
- [foo](https://foo.com) - VaLid description.
- [foo](https://foo.com) - anoTher valid description with pascalCase.

- [foo](https://foo.com) - Ending with a period.
- [foo](https://foo.com) - Ending with an exclamation mark!
- [foo](https://foo.com) - Ending with a question mark?
- [foo](https://foo.com) - Ending with an ellipsis…

- [foo](https://foo.com) - Ending with a "non-quoted period".
- [foo](https://foo.com) - Ending with a "non-quoted exclamation point"!
- [foo](https://foo.com) - Ending with a "non-quoted question mark"?
- [foo](https://foo.com) - Ending with a "non-quoted ellipsis"…
- [foo](https://foo.com) - Ending with another kind of “non-quoted period”.
- [foo](https://foo.com) - Ending with another kind of “non-quoted exclamation point”!
- [foo](https://foo.com) - Ending with another kind of “non-quoted question mark”?
- [foo](https://foo.com) - Ending with another kind of “non-quoted ellipsis”…

- [foo](https://foo.com) - "Description is a full quote ending with a period."
- [foo](https://foo.com) - "Description is a full quote ending with an exclamation point!"
- [foo](https://foo.com) - "Description is a full quote ending with a question mark?"
- [foo](https://foo.com) - "Description is a full quote ending with an ellipsis…"
- [foo](https://foo.com) - “Description is an other king of full quote ending with a period.”
- [foo](https://foo.com) - “Description is an other king of full quote ending with an exclamation point!”
- [foo](https://foo.com) - “Description is an other king of full quote ending with a question mark?”
- [foo](https://foo.com) - “Description is an other king of full quote ending with an ellipsis…”

- [foo](https://foo.com) - Ending with a parenthetical. (Japanese)
- [foo](https://foo.com) - Ending with an emphasis parenthetical. *(Japanese)*
- [foo](https://foo.com) - Ending with a strong parenthetical. **(Japanese)**

- [foo](https://foo.com) - Ending with an emoji case 1. 📷
- [foo](https://foo.com) - Ending with an emoji case 1. 📷 📷 📷
- [foo](https://foo.com) - Ending with an emoji case 2. 👩🏿
- [foo](https://foo.com) - Ending with an emoji case 3. ⌚

- [foo](https://foo.com) - [Preview](https://read.amazon.com/kp/embed?asin=B01G7TTKSK&asin=B01G7TTKSK&preview=newtab&linkCode=kpe&ref_=cm_sw_r_kb_dp_DLhOxb0XZ3MEC) 💲
"""


invalid = """
- [foo](https://foo.com) - Missing ending punctuation

- [foo](https://foo.com) - `invalid quote: too noisy`
- [foo](https://foo.com) - `still invalid quote, even with a period.`
- [foo](https://foo.com) - `still invalid quote, even with an exclamation mark!`
- [foo](https://foo.com) - `still invalid quote, even with a question mark?`
- [foo](https://foo.com) - `still invalid quote, even with an ellipsis…`
- [foo](https://foo.com) - `still invalid quote, even ending with a period`.
- [foo](https://foo.com) - `still invalid quote, even ending with an exclamation mark`!
- [foo](https://foo.com) - `still invalid quote, even ending with a question mark`?
- [foo](https://foo.com) - `still invalid quote, even ending with an ellipsis`…

- [foo](https://foo.com) - Quote-inducing double punctuation "end.".
- [foo](https://foo.com) - Quote-inducing double punctuation "end."!
- [foo](https://foo.com) - Quote-inducing double punctuation "end."?
- [foo](https://foo.com) - Quote-inducing double punctuation "end."…
- [foo](https://foo.com) - Quote-inducing double punctuation "end!".
- [foo](https://foo.com) - Quote-inducing double punctuation "end!"!
- [foo](https://foo.com) - Quote-inducing double punctuation "end!"?
- [foo](https://foo.com) - Quote-inducing double punctuation "end!"…
- [foo](https://foo.com) - Quote-inducing double punctuation "end?".
- [foo](https://foo.com) - Quote-inducing double punctuation "end?"!
- [foo](https://foo.com) - Quote-inducing double punctuation "end?"?
- [foo](https://foo.com) - Quote-inducing double punctuation "end?"…
- [foo](https://foo.com) - Quote-inducing double punctuation "end…".
- [foo](https://foo.com) - Quote-inducing double punctuation "end…"!
- [foo](https://foo.com) - Quote-inducing double punctuation "end…"?
- [foo](https://foo.com) - Quote-inducing double punctuation "end…"…

- [foo](https://foo.com) - Quote-inducing double punctuation “end.”.
- [foo](https://foo.com) - Quote-inducing double punctuation “end.”!
- [foo](https://foo.com) - Quote-inducing double punctuation “end.”?
- [foo](https://foo.com) - Quote-inducing double punctuation “end.”…
- [foo](https://foo.com) - Quote-inducing double punctuation “end!”.
- [foo](https://foo.com) - Quote-inducing double punctuation “end!”!
- [foo](https://foo.com) - Quote-inducing double punctuation “end!”?
- [foo](https://foo.com) - Quote-inducing double punctuation “end!”…
- [foo](https://foo.com) - Quote-inducing double punctuation “end?”.
- [foo](https://foo.com) - Quote-inducing double punctuation “end?”!
- [foo](https://foo.com) - Quote-inducing double punctuation “end?”?
- [foo](https://foo.com) - Quote-inducing double punctuation “end?”…
- [foo](https://foo.com) - Quote-inducing double punctuation “end…”.
- [foo](https://foo.com) - Quote-inducing double punctuation “end…”!
- [foo](https://foo.com) - Quote-inducing double punctuation “end…”?
- [foo](https://foo.com) - Quote-inducing double punctuation “end…”…
"""


def ends_with_emoji(string):
    # Quick and dirty check for emoji. Good enough for our tests here.
    return ord(string[-1]) > 2**16


def validate(line):
    """ Python translation of ./rules/list-item.js:validateListItemSuffix() """

    desc = line.split(' - ', 1)[1]
    # print(desc)

    # XXX NEW RULE
    # Descriptions ending with any number of regular punctuation are OK.
    #print('Rule #1')
    #if re.match(r".*[.?!…]+$", desc):
    #    return True

    # XXX NEW RULE
    print('Rule #1')
    # Descriptions are not allowed to be fully backticked quotes, whatever the
    # ending punctuation and its position.
    if re.match(r"^`.*[.?!…]*`[.?!…]*$", desc):
        # Still allow multiple backticks if the whole description is not fully
        # quoted.
        if re.match(r"^`.+`.+`.+$", desc):
            return True
        return False

    # XXX NEW RULE
    # Any kind of quote followed by one of our punctuaction marker is perfect,
    # but only if not following a punctuation itself.
    print('Rule #3')
    # Use positive lookbehind to search for punctuation following a quote.
    if re.match(r".*(?<=(\"|”))[.?!…]$", desc):
        # If the quote follows a regular punctuation, this is wrong.
        if re.match(r".*[.?!…](\"|”)[.?!…]$", desc):
            return False
        return True

    # XXX NEW RULE
    # Any of our punctuation marker eventually closed by any kind of quote is
    # good.
    print('Rule #4')
    if re.match(r".*[.?!…][\"”]?$", desc):
        return True

    # XXX ALREADY IMPLEMENTED
    print('Rule #5')
    if not re.findall(r'[\.?!…]', desc):
        tokens = [t for t in re.split(r"[- ;./]", desc) if t]
        if len(tokens) > 2 or not ends_with_emoji(tokens[-1]):
            return False

    # XXX ALREADY IMPLEMENTED
    print('Rule #6')
    if re.match(r".*\)?$", desc):
        return True

    # XXX ALREADY IMPLEMENTED
    print('Rule #7')
    if ends_with_emoji(desc):
        return True

    return False


for line in [l for l in valid.splitlines() if l]:
    print("Must be valid: {}".format(line))
    assert validate(line)


for line in [l for l in invalid.splitlines() if l]:
    print("Must be invalid: {}".format(line))
    assert not validate(line)
```

This covers all the cases.

TODO:
* [ ] translate Python logic to JS
* [ ] fix travis
kdeldycke: This PR is ready for review. At last! :)

Patch:
diff --git a/index.js b/index.js
index c513b7c..e7f2141 100644
--- a/index.js
+++ b/index.js
@@ -35,7 +35,7 @@ const lint = options => {
 		plugins: options.config
 	}];
 
-	const codeOfConductFile = globby.sync(['{.github/,}{code-of-conduct,code_of_conduct}.md'], {nocase: true, cwd: dirname})[0];
+	const codeOfConductFile = globby.sync(['{code-of-conduct,code_of_conduct}.md', '.github/{code-of-conduct,code_of_conduct}.md'], {nocase: true, cwd: dirname})[0];
 	if (codeOfConductFile) {
 		const codeOfConductVFile = toVfile.readSync(path.resolve(dirname, codeOfConductFile));
 		codeOfConductVFile.repoURL = options.repoURL;
diff --git a/rules/contributing.js b/rules/contributing.js
index 693be33..7f44b37 100644
--- a/rules/contributing.js
+++ b/rules/contributing.js
@@ -7,7 +7,7 @@ const rule = require('unified-lint-rule');
 module.exports = rule('remark-lint:awesome-contributing', (ast, file) => {
 	const {dirname} = file;
 
-	const contributingFile = globby.sync(['{.github/,}contributing.md'], {nocase: true, cwd: dirname})[0];
+	const contributingFile = globby.sync(['contributing.md', '.github/contributing.md'], {nocase: true, cwd: dirname})[0];
 	// TODO: This doesn't work on Linux for some reason. Investigate and then open an issue on `fast-glob`.
 	// const contributingFile = globby.sync('contributing.md', {case: false, cwd: dirname})[0];
 
diff --git a/rules/list-item.js b/rules/list-item.js
index da6688e..22da87c 100644
--- a/rules/list-item.js
+++ b/rules/list-item.js
@@ -276,12 +276,39 @@ function validateListItemPrefix(descriptionText, prefixText) {
 }
 
 function validateListItemSuffix(descriptionText, suffixText) {
-	if (/[.!?…]\s*$/.test(suffixText)) {
-		// Description ends with '.', '!', '?' or '…'
+	// Punctuation rules are available at: https://www.thepunctuationguide.com
+
+	// Descriptions are not allowed to be fully backticked quotes, whatever the
+	// ending punctuation and its position.
+	if (/^`.*[.!?…]*`[.!?…]*$/.test(descriptionText)) {
+		// Still allow multiple backticks if the whole description is not fully
+		// quoted.
+		if (/^`.+`.+`.+$/.test(descriptionText)) {
+			return true;
+		}
+
+		return false;
+	}
+
+	// Any kind of quote followed by one of our punctuaction marker is perfect,
+	// but only if not following a punctuation itself. Uses positive lookbehind
+	// to search for punctuation following a quote.
+	if (/.*(?<=["”])[.!?…]+$/.test(descriptionText)) {
+		// If the quote follows a regular punctuation, this is wrong.
+		if (/.*[.!?…]["”][.!?…]+$/.test(descriptionText)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	// Any of our punctuation marker eventually closed by any kind of quote is
+	// good.
+	if (/.*[.!?…]["”]?$/.test(descriptionText)) {
 		return true;
 	}
 
-	if (!/[.!?]/.test(descriptionText)) {
+	if (!/[.!?…]/.test(descriptionText)) {
 		// Description contains no punctuation
 		const tokens = tokenizeWords(descriptionText);
 		if (tokens.length > 2 || !textEndsWithEmoji(tokens[tokens.length - 1])) {


Test patch:
diff --git a/test/fixtures/contributing/valid2/.github/CONTRIBUTING.md b/test/fixtures/contributing/valid2/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..df1fc73
--- /dev/null
+++ b/test/fixtures/contributing/valid2/.github/CONTRIBUTING.md
@@ -0,0 +1,30 @@
+# Contribution Guidelines
+
+Please note that this project is released with a [Contributor Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms.
+
+---
+
+Ensure your pull request adheres to the following guidelines:
+
+- **If you just created something, wait at least 7 days before submitting.** This is to give it some time to mature and ensure it's not just a publish-and-forget type of project.
+- If you submit a project that is similar to an existing project in the list, argue how it's better.
+- Search previous suggestions before making a new one, as yours may be a duplicate.
+- Suggested packages should be tested and documented.
+- Make an individual pull request for each suggestion.
+- Use the following format: `[package](link) - Description.`
+- Additions should be added to the bottom of the relevant category.
+- Link to the GitHub repo, not npmjs.com.
+- Keep descriptions short and simple, but descriptive.
+- Don't mention `Node.js` in the description as it's implied.
+- Start the description with a capital and end with a full stop/period.
+- Don't start the description with `A` or `An`.
+- Check your spelling and grammar.
+- Make sure your text editor is set to remove trailing whitespace.
+- The pull request should have a useful title and include a link to the package and why it should be included.
+- New categories or improvements to the existing categorization are welcome, but should be done in a separate pull request.
+
+Thank you for your suggestions!
+
+### Updating your PR
+
+A lot of times, making a PR adhere to the standards above can be difficult. If the maintainers notice anything that we'd like changed, we'll ask you to edit your PR before we merge it. If you're not sure how to do that, [here is a guide](https://github.com/RichardLitt/knowledge/blob/master/github/amending-a-commit-guide.md) on the different ways you can update your PR so that we can merge it.
diff --git a/test/fixtures/contributing/valid2/readme.md b/test/fixtures/contributing/valid2/readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/contributing/valid3/.github/contributing.md b/test/fixtures/contributing/valid3/.github/contributing.md
new file mode 100644
index 0000000..df1fc73
--- /dev/null
+++ b/test/fixtures/contributing/valid3/.github/contributing.md
@@ -0,0 +1,30 @@
+# Contribution Guidelines
+
+Please note that this project is released with a [Contributor Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms.
+
+---
+
+Ensure your pull request adheres to the following guidelines:
+
+- **If you just created something, wait at least 7 days before submitting.** This is to give it some time to mature and ensure it's not just a publish-and-forget type of project.
+- If you submit a project that is similar to an existing project in the list, argue how it's better.
+- Search previous suggestions before making a new one, as yours may be a duplicate.
+- Suggested packages should be tested and documented.
+- Make an individual pull request for each suggestion.
+- Use the following format: `[package](link) - Description.`
+- Additions should be added to the bottom of the relevant category.
+- Link to the GitHub repo, not npmjs.com.
+- Keep descriptions short and simple, but descriptive.
+- Don't mention `Node.js` in the description as it's implied.
+- Start the description with a capital and end with a full stop/period.
+- Don't start the description with `A` or `An`.
+- Check your spelling and grammar.
+- Make sure your text editor is set to remove trailing whitespace.
+- The pull request should have a useful title and include a link to the package and why it should be included.
+- New categories or improvements to the existing categorization are welcome, but should be done in a separate pull request.
+
+Thank you for your suggestions!
+
+### Updating your PR
+
+A lot of times, making a PR adhere to the standards above can be difficult. If the maintainers notice anything that we'd like changed, we'll ask you to edit your PR before we merge it. If you're not sure how to do that, [here is a guide](https://github.com/RichardLitt/knowledge/blob/master/github/amending-a-commit-guide.md) on the different ways you can update your PR so that we can merge it.
diff --git a/test/fixtures/contributing/valid3/readme.md b/test/fixtures/contributing/valid3/readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/list-item/0.md b/test/fixtures/list-item/0.md
index 1ea9e3d..b032dfa 100644
--- a/test/fixtures/list-item/0.md
+++ b/test/fixtures/list-item/0.md
@@ -3,18 +3,22 @@ All list-items in this document should be linted as **valid**.
 - [foo](https://foo.com) - Valid description.
 - [foo](https://foo.com) - A valid description.
 - [foo](https://foo.com) - A valid description...
+- [foo](https://foo.com) - A valid description.......
 - [foo](https://foo.com) - A valid description…
+- [foo](https://foo.com) - A valid description..….....
 - [foo](https://foo.com) - A valid description!
 - [foo](https://foo.com) - A valid description! ⭐
 - [foo](https://foo.com) - A valid description!!!
 - [foo](https://foo.com) - A valid description?
 - [foo](https://foo.com) - A valid description???
 - [foo](https://foo.com) - A valid description????!??
+- [foo](https://foo.com) - A valid description..…!?…....
 - [foo](https://foo.com) - A valid description with [link](https://bar.org).
 - [foo](https://foo.com) - A valid description. ![image](image.png)
 - [foo](https://foo.com) - A valid description. <img src="image.png">
 - [foo](https://foo.com) - `valid description` here.
-- [foo](https://foo.com) - `valid description`.
+- [foo](https://foo.com) - `valid` is a word that is `great`.
+- [foo](https://foo.com) - `valid` is a word that is `great`!?!.
 - [foo](https://foo.com) - VaLid description.
 - [foo](https://foo.com) - anoTher valid description with pascalCase.
   - [foo](https://foo.com) - Valid sub-item.
@@ -44,6 +48,30 @@ These are valid list items that don't need a description.
 - [foo](https://foo.com) - "Catan" inspired board game.
 - [foo](https://foo.com) - 'About This App' window.
 
+Test description's ending punctuation.
+- [foo](https://foo.com) - Ending with a period.
+- [foo](https://foo.com) - Ending with an exclamation mark!
+- [foo](https://foo.com) - Ending with a question mark?
+- [foo](https://foo.com) - Ending with an ellipsis…
+
+- [foo](https://foo.com) - Ending with a "non-quoted period".
+- [foo](https://foo.com) - Ending with a "non-quoted exclamation point"!
+- [foo](https://foo.com) - Ending with a "non-quoted question mark"?
+- [foo](https://foo.com) - Ending with a "non-quoted ellipsis"…
+- [foo](https://foo.com) - Ending with another kind of “non-quoted period”.
+- [foo](https://foo.com) - Ending with another kind of “non-quoted exclamation point”!
+- [foo](https://foo.com) - Ending with another kind of “non-quoted question mark”?
+- [foo](https://foo.com) - Ending with another kind of “non-quoted ellipsis”…
+
+- [foo](https://foo.com) - "Description is a full quote ending with a period."
+- [foo](https://foo.com) - "Description is a full quote ending with an exclamation point!"
+- [foo](https://foo.com) - "Description is a full quote ending with a question mark?"
+- [foo](https://foo.com) - "Description is a full quote ending with an ellipsis…"
+- [foo](https://foo.com) - “Description is an other king of full quote ending with a period.”
+- [foo](https://foo.com) - “Description is an other king of full quote ending with an exclamation point!”
+- [foo](https://foo.com) - “Description is an other king of full quote ending with a question mark?”
+- [foo](https://foo.com) - “Description is an other king of full quote ending with an ellipsis…”
+
 - [foo](https://foo.com) - Ending with a parenthetical. (Japanese)
 - [foo](https://foo.com) - Ending with an emphasis parenthetical. *(Japanese)*
 - [foo](https://foo.com) - Ending with a strong parenthetical. **(Japanese)**
diff --git a/test/fixtures/list-item/1.md b/test/fixtures/list-item/1.md
index f3dd6af..f25e0ec 100644
--- a/test/fixtures/list-item/1.md
+++ b/test/fixtures/list-item/1.md
@@ -3,7 +3,6 @@ All list-items in this document should be linted as **invalid**.
 - [foo](https://foo.com) - an invalid description.
 - [foo](https://foo.com) - invalid description.
 - [foo](https://foo.com)  -  invalid dash separator.
-- [foo](https://foo.com) - `invalid description.`
 - [foo]() - Invalid url.
 - [foo](test) - Invalid url.
 - [](https://foo.com) - Invalid link text.
@@ -15,3 +14,57 @@ All list-items in this document should be linted as **invalid**.
 
 - [sparkly](https://github.com/sindresorhus/sparkly) - Generate sparklines ▁▂▃▅▂▇
 - [cat-ascii-faces](https://github.com/melaniecebula/cat-ascii-faces) - ₍˄·͈༝·͈˄₎◞ ̑̑ෆ⃛ (=ↀωↀ=)✧ (^･o･^)ﾉ”
+
+Test description's ending punctuation.
+- [foo](https://foo.com) - Missing ending punctuation
+
+- [foo](https://foo.com) - ``
+- [foo](https://foo.com) - `invalid quote: too noisy`
+- [foo](https://foo.com) - `still invalid quote, even with a period.`
+- [foo](https://foo.com) - `still invalid quote, even with an exclamation mark!`
+- [foo](https://foo.com) - `still invalid quote, even with a question mark?`
+- [foo](https://foo.com) - `still invalid quote, even with an ellipsis…`
+- [foo](https://foo.com) - `still invalid quote, even ending with a period`.
+- [foo](https://foo.com) - `still invalid quote, even ending with an exclamation mark`!
+- [foo](https://foo.com) - `still invalid quote, even ending with a question mark`?
+- [foo](https://foo.com) - `still invalid quote, even ending with an ellipsis`…
+
+- [foo](https://foo.com) - `still invalid quote, ending with too much punctuations`…...?
+- [foo](https://foo.com) - `still invalid quote, ending with too much punctuations...!?`…...?
+
+- [foo](https://foo.com) - Quote-inducing double punctuation "end.".
+- [foo](https://foo.com) - Quote-inducing double punctuation "end."!
+- [foo](https://foo.com) - Quote-inducing double punctuation "end."?
+- [foo](https://foo.com) - Quote-inducing double punctuation "end."…
+- [foo](https://foo.com) - Quote-inducing double punctuation "end!".
+- [foo](https://foo.com) - Quote-inducing double punctuation "end!"!
+- [foo](https://foo.com) - Quote-inducing double punctuation "end!"?
+- [foo](https://foo.com) - Quote-inducing double punctuation "end!"…
+- [foo](https://foo.com) - Quote-inducing double punctuation "end?".
+- [foo](https://foo.com) - Quote-inducing double punctuation "end?"!
+- [foo](https://foo.com) - Quote-inducing double punctuation "end?"?
+- [foo](https://foo.com) - Quote-inducing double punctuation "end?"…
+- [foo](https://foo.com) - Quote-inducing double punctuation "end…".
+- [foo](https://foo.com) - Quote-inducing double punctuation "end…"!
+- [foo](https://foo.com) - Quote-inducing double punctuation "end…"?
+- [foo](https://foo.com) - Quote-inducing double punctuation "end…"…
+
+- [foo](https://foo.com) - Quote-inducing double punctuation “end.”.
+- [foo](https://foo.com) - Quote-inducing double punctuation “end.”!
+- [foo](https://foo.com) - Quote-inducing double punctuation “end.”?
+- [foo](https://foo.com) - Quote-inducing double punctuation “end.”…
+- [foo](https://foo.com) - Quote-inducing double punctuation “end!”.
+- [foo](https://foo.com) - Quote-inducing double punctuation “end!”!
+- [foo](https://foo.com) - Quote-inducing double punctuation “end!”?
+- [foo](https://foo.com) - Quote-inducing double punctuation “end!”…
+- [foo](https://foo.com) - Quote-inducing double punctuation “end?”.
+- [foo](https://foo.com) - Quote-inducing double punctuation “end?”!
+- [foo](https://foo.com) - Quote-inducing double punctuation “end?”?
+- [foo](https://foo.com) - Quote-inducing double punctuation “end?”…
+- [foo](https://foo.com) - Quote-inducing double punctuation “end…”.
+- [foo](https://foo.com) - Quote-inducing double punctuation “end…”!
+- [foo](https://foo.com) - Quote-inducing double punctuation “end…”?
+- [foo](https://foo.com) - Quote-inducing double punctuation “end…”…
+
+- [foo](https://foo.com) - Quote-inducing double punctuation “end…”??!
+- [foo](https://foo.com) - Quote-inducing double punctuation “end?…...”…...?
diff --git a/test/rules/contributing.js b/test/rules/contributing.js
index 72c7d39..498deb4 100644
--- a/test/rules/contributing.js
+++ b/test/rules/contributing.js
@@ -38,3 +38,13 @@ test('contributing - valid contributing.md', async t => {
 	const messages = await lint({config, filename: 'test/fixtures/contributing/valid1/readme.md'});
 	t.deepEqual(messages, []);
 });
+
+test('contributing - valid .github/CONTRIBUTING.md', async t => {
+	const messages = await lint({config, filename: 'test/fixtures/contributing/valid2/readme.md'});
+	t.deepEqual(messages, []);
+});
+
+test('contributing - valid .github/contributing.md', async t => {
+	const messages = await lint({config, filename: 'test/fixtures/contributing/valid3/readme.md'});
+	t.deepEqual(messages, []);
+});
diff --git a/test/rules/snapshots/list-item.js.md b/test/rules/snapshots/list-item.js.md
index c41f289..4f05c1f 100644
--- a/test/rules/snapshots/list-item.js.md
+++ b/test/rules/snapshots/list-item.js.md
@@ -53,7 +53,7 @@ Generated by [AVA](https://ava.li).
       },
       {
         line: 6,
-        message: 'List item description must end with proper punctuation',
+        message: 'Invalid list item link URL',
         ruleId: 'awesome-list-item',
       },
       {
@@ -63,22 +63,22 @@ Generated by [AVA](https://ava.li).
       },
       {
         line: 8,
-        message: 'Invalid list item link URL',
+        message: 'Invalid list item link text',
         ruleId: 'awesome-list-item',
       },
       {
-        line: 9,
-        message: 'Invalid list item link text',
+        line: 12,
+        message: 'List item link and description separated by invalid whitespace',
         ruleId: 'awesome-list-item',
       },
       {
         line: 13,
-        message: 'List item link and description separated by invalid whitespace',
+        message: 'List item link and description separated by invalid en-dash',
         ruleId: 'awesome-list-item',
       },
       {
-        line: 14,
-        message: 'List item link and description separated by invalid en-dash',
+        line: 15,
+        message: 'List item description must end with proper punctuation',
         ruleId: 'awesome-list-item',
       },
       {
@@ -87,8 +87,8 @@ Generated by [AVA](https://ava.li).
         ruleId: 'awesome-list-item',
       },
       {
-        line: 17,
-        message: 'List item description must end with proper punctuation',
+        line: 9,
+        message: 'List item description must start with valid casing',
         ruleId: 'awesome-list-item',
       },
       {
@@ -97,8 +97,208 @@ Generated by [AVA](https://ava.li).
         ruleId: 'awesome-list-item',
       },
       {
-        line: 11,
-        message: 'List item description must start with valid casing',
+        line: 19,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 21,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 22,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 23,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 24,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 25,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 26,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 35,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 36,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 37,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 38,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 39,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 40,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 41,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 42,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 43,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 44,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 45,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 46,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 47,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 48,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 49,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 50,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 52,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 53,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 54,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 55,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 56,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 57,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 58,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 59,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 60,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 61,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 62,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 63,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 64,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 65,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 66,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 67,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 69,
+        message: 'List item description must end with proper punctuation',
+        ruleId: 'awesome-list-item',
+      },
+      {
+        line: 70,
+        message: 'List item description must end with proper punctuation',
         ruleId: 'awesome-list-item',
       },
     ]