diff --git a/.github/workflows/lint-patterns.yml b/.github/workflows/lint-patterns.yml new file mode 100644 index 000000000..c8140e85e --- /dev/null +++ b/.github/workflows/lint-patterns.yml @@ -0,0 +1,32 @@ +# from: https://github.com/marketplace/actions/markdown-linting-action + +name: Validate pattern syntax + +on: [push, pull_request] +jobs: + validate-pattern-syntax: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Problem Matcher for markdownlint-cli + uses: xt0rted/markdownlint-problem-matcher@v1 + - name: Lint pattern files (markdown) + uses: avto-dev/markdown-lint@v1 + with: + rules: './lint/pattern-template.js' + config: './lint/pattern-template.yml' + # ignore-path: './lint/.markdownlintignore' + ignore: "+(README|CONTRIBUTING|TRUSTED-COMMITTERS).md" + args: './*.md' + +# +# on: push +# jobs: +# build: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - uses: xt0rted/markdownlint-problem-matcher@v1 +# - run: npm install -g markdownlint-cli +# - run: markdownlint --ignore-path ./lint/.markdownlintignore -r ./lint/pattern-template.js -c ./lint/pattern-template.yml ./*.md diff --git a/crossteam-project-valuation.md b/crossteam-project-valuation.md index 6eec411fa..23bc7178d 100644 --- a/crossteam-project-valuation.md +++ b/crossteam-project-valuation.md @@ -7,3 +7,7 @@ Cross Team Valuation It's hard to sell the value of cross-team, inner sourced projects that don't provide a direct impact on company revenue. Here's a data-driven way to represent your project that both articulates its value and amplifies it. **This pattern is currently [under development in a Pull Request](https://github.com/InnerSourceCommons/InnerSourcePatterns/pull/87)** + +# markdownlint tester + +This is just a test. diff --git a/lint/.markdownlintignore b/lint/.markdownlintignore new file mode 100644 index 000000000..1ae3df4ef --- /dev/null +++ b/lint/.markdownlintignore @@ -0,0 +1,4 @@ +**/README.md +**/TRUSTED-COMMITTERS.md +**/CONTRIBUTING.md +**/IS.* diff --git a/lint/pattern-template.js b/lint/pattern-template.js new file mode 100644 index 000000000..5cc60d3c3 --- /dev/null +++ b/lint/pattern-template.js @@ -0,0 +1,117 @@ +"use strict"; + +module.exports = [ +{ + names: ["PATTERN-TEMPLATE-RULE-001"], + description: "No h1 headlines", + tags: ["headings", "headers", "pattern-template"], + function: (params, onError) => { + params.tokens.filter(function filterToken(token) { + return token.type === "heading_open"; + }).forEach(function forToken(token) { + if (token.tag === "h1") { + return onError({ + lineNumber: token.lineNumber, + detail: "Use of #-headlines (h1) is not allowed in patterns", + context: token.line + }); + } + }); + } +}, +{ + names: ["PATTERN-TEMPLATE-RULE-002"], + description: "Standard Headlines", + tags: ["headings", "headers", "pattern-template"], + function: (params, onError) => { + var allowedHeadlines = "Title|Patlet|Problem|Story|Context|Forces|Solutions|Resulting Context|Known Instances|Status|Author(s)|Acknowledgements"; + var re = new RegExp(`^## (${allowedHeadlines.replace(/(?=[\(\)])/g, '\\')})\\s*$`,"m"); + // console.log(re); + + params.tokens.filter(function filterToken(token) { + return token.type === "heading_open"; + }).forEach(function forToken(token) { + if (token.tag === "h2") { + if (re.test(token.line)) { + return; + } + // if (/^## ()$/m.test(token.line)) { + // return; + // } + + return onError({ + lineNumber: token.lineNumber, + detail: "Allowed types are: " + allowedHeadlines.replace(/\|/g, ', '), + context: token.line + }); + } + }); + } +}, +{ + names: ["PATTERN-TEMPLATE-RULE-003"], + description: "Mandatory template sections", + tags: ["headings", "headers", "pattern-template"], + function: (params, onError) => { + var mandatoryHeadlines = { + "Title": "Title", + "Patlet": "Patlet", + "Problem": "Problem", + "Context": "Context", + "Forces": "Forces", + "Solutions": "Solutions?", + "Resulting Context": "Resulting Context", + "Status": "Status", + "Author(s)": "(Author\\(s\\))|(Authors?)" + }; + + // console.log(mandatoryHeadlines); + + var collectedHeadlines = "" + + // collect all h2 headlines + // (only the headline text itself, removing markdown sytnax and whitespace) + params.tokens.filter(function filterToken(token) { + return token.type === "heading_open"; + }).forEach(function forToken(token) { + if (token.tag === "h2") { + let re = new RegExp("^## (.*?)\\s*$","m"); + let matchResult = token.line.match(re); + + if (matchResult != null) { + collectedHeadlines += matchResult[1] + "\n" + } + } + }); + + // confirm if all `mandatoryHeadlines` exist exactly once in the `collectedHeadlines` + var errorsFound = []; + + let headline; + for (headline in mandatoryHeadlines){ + // console.log(headline); + let pattern = mandatoryHeadlines[headline]; + let re = new RegExp(`^${pattern}$`,"gm"); + let matchResult = collectedHeadlines.match(re); + + // TODO as soon as I use the option 'g' in the regexp, it does not return a full match array anymore + if (matchResult != null) { + if (matchResult.length >= 2 ) { + errorsFound.push(`Duplicate headline "${headline}".`); + } + } + else { + errorsFound.push(`Required headline "${headline}" is missing.`); + } + } + + // if any errors were found, raise a linter error + if (errorsFound.length > 0) { + return onError({ + lineNumber: 1, + detail: errorsFound.join(" ") + }); + } + } +} +]; diff --git a/lint/pattern-template.yml b/lint/pattern-template.yml new file mode 100644 index 000000000..61c05d28c --- /dev/null +++ b/lint/pattern-template.yml @@ -0,0 +1,6 @@ +default: false # includes/excludes all rules by default + +# Custom rules: +PATTERN-TEMPLATE-RULE-001: true +# PATTERN-TEMPLATE-RULE-002: true +PATTERN-TEMPLATE-RULE-003: true