Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 2 New Header Rules and Fix Issue with Yaml Title Grabbing Any Header #520

Merged
merged 2 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions __tests__/yaml-title.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,21 @@ ruleTest({
# This is a [[Heading]]
`,
},
{ // accounts for https://github.com/platers/obsidian-linter/issues/519
testName: 'Make sure that the first header is captured even if another header precedes it.',
before: dedent`
### not the title

# Title
`,
after: dedent`
---
title: Title
---
### not the title

# Title
`,
},
],
});
60 changes: 60 additions & 0 deletions src/rules/headings-start-line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Options, RuleType} from '../rules';
import RuleBuilder, {ExampleBuilder, OptionBuilderBase} from './rule-builder';
import dedent from 'ts-dedent';
import {ignoreListOfTypes, IgnoreTypes} from '../utils/ignore-types';
import {allHeadersRegex} from '../utils/regex';

class HeadingStartLineOptions implements Options {
}

@RuleBuilder.register
export default class HeadingStartLine extends RuleBuilder<HeadingStartLineOptions> {
get OptionsClass(): new () => HeadingStartLineOptions {
return HeadingStartLineOptions;
}
get name(): string {
return 'Headings Start Line';
}
get description(): string {
return 'Headings that do not start a line will have their preceding whitespace removed to make sure they get recognized as headers.';
}
get type(): RuleType {
return RuleType.HEADING;
}
apply(text: string, options: HeadingStartLineOptions): string {
return ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.yaml], text, (text) => {
return text.replaceAll(allHeadersRegex, (heading: string) => {
return heading.trimStart();
});
});
}
get exampleBuilders(): ExampleBuilder<HeadingStartLineOptions>[] {
return [
new ExampleBuilder({
description: 'Removes spaces prior to a heading',
before: dedent`
${''} ## Other heading preceded by 2 spaces ##
_Note that if the spacing is enough for the header to be considered to be part of a codeblock it will not be affected by this rule._
`,
after: dedent`
## Other heading preceded by 2 spaces ##
_Note that if the spacing is enough for the header to be considered to be part of a codeblock it will not be affected by this rule._
`,
}),
new ExampleBuilder({
description: 'Tags are not affected by this',
before: dedent`
${''} #test
${''} # Heading &amp;
`,
after: dedent`
${''} #test
# Heading &amp;
`,
}),
];
}
get optionBuilders(): OptionBuilderBase<HeadingStartLineOptions>[] {
return [];
}
}
80 changes: 80 additions & 0 deletions src/rules/remove-trailing-punctuation-in-heading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {Options, RuleType} from '../rules';
import RuleBuilder, {ExampleBuilder, OptionBuilderBase, TextOptionBuilder} from './rule-builder';
import dedent from 'ts-dedent';
import {ignoreListOfTypes, IgnoreTypes} from '../utils/ignore-types';
import {allHeadersRegex} from '../utils/regex';

class RemoveTrailingPunctuationInHeadingOptions implements Options {
punctuationToRemove?: string = '.,;:!。,;:!';
}

@RuleBuilder.register
export default class RemoveTrailingPunctuationInHeading extends RuleBuilder<RemoveTrailingPunctuationInHeadingOptions> {
get OptionsClass(): new () => RemoveTrailingPunctuationInHeadingOptions {
return RemoveTrailingPunctuationInHeadingOptions;
}
get name(): string {
return 'Remove Trailing Punctuation in Heading';
}
get description(): string {
return 'Removes the specified punctuation from the end of headings making sure to ignore the semicolon at the end of [HTML entity references](https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references).';
}
get type(): RuleType {
return RuleType.HEADING;
}
apply(text: string, options: RemoveTrailingPunctuationInHeadingOptions): string {
return ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.yaml], text, (text) => {
return text.replaceAll(allHeadersRegex,
(heading: string, $1: string = '', $2: string = '', $3: string = '', $4: string = '', $5: string = '') => {
// ignore the html entities and entries without any heading text
// regex from https://stackoverflow.com/a/26128757/8353749
if ($4 == '' || $4.match(/&[^\s]+;$/mi)) {
return heading;
}

const lastHeadingChar = $4.charAt($4.length - 1);
if (options.punctuationToRemove.includes(lastHeadingChar)) {
return $1 + $2 + $3 + $4.substring(0, $4.length - 1) + $5;
}

return heading;
});
});
}
get exampleBuilders(): ExampleBuilder<RemoveTrailingPunctuationInHeadingOptions>[] {
return [
new ExampleBuilder({
description: 'Removes punctuation from the end of a heading',
before: dedent`
# Heading ends in a period.
## Other heading ends in an exclamation mark! ##
`,
after: dedent`
# Heading ends in a period
## Other heading ends in an exclamation mark ##
`,
}),
new ExampleBuilder({
description: 'HTML Entities at the end of a heading is ignored',
before: dedent`
# Heading 1
## Heading &amp;
`,
after: dedent`
# Heading 1
## Heading &amp;
`,
}),
];
}
get optionBuilders(): OptionBuilderBase<RemoveTrailingPunctuationInHeadingOptions>[] {
return [
new TextOptionBuilder({
OptionsClass: RemoveTrailingPunctuationInHeadingOptions,
name: 'Trailing Punctuation',
description: 'The trailing punctuation to remove from the headings in the file.',
optionsKey: 'punctuationToRemove',
}),
];
}
}
9 changes: 5 additions & 4 deletions src/utils/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import {makeSureContentHasEmptyLinesAddedBeforeAndAfter} from './strings';

// Useful regexes
export const headerRegex = /^(\s*)(#+)(\s+)(.*)$/m;
export const headerRegex = /^([ \t]*)(#+)([ \t]+)([^#\n\r]*)([ \t]+#+)?$/m;
export const allHeadersRegex = new RegExp(headerRegex.source, headerRegex.flags + 'g');
export const fencedRegexTemplate = '^XXX\\.*?\n(?:((?:.|\n)*?)\n)?XXX(?=\\s|$)$';
export const yamlRegex = /^---\n((?:(((?!---)(?:.|\n)*?)\n)?))---(?=\n|$)/;
export const backtickBlockRegexTemplate = fencedRegexTemplate.replaceAll('X', '`');
Expand Down Expand Up @@ -88,9 +89,9 @@ export function ensureEmptyLinesAroundTables(text: string): string {
* @return {string} The text for the first header one if present or an empty string.
*/
export function getFirstHeaderOneText(text: string) {
const result = text.match(headerRegex);
if (result && result[4]) {
let headerText = result[4];
const result = text.match(/^#\s+(.*)/m);
if (result && result[1]) {
let headerText = result[1];
headerText = headerText.replaceAll(wikiLinkRegex, (_, _2, $2: string, $3: string) => {
return $3 ?? $2;
});
Expand Down