Repository: platers/obsidian-linter (ts)

Issue:
FR: Remove Space Prior to English Punctuation and Full Width Characters
在践行[中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)的时候容易多打空格，请问能否用Linter删除以下示例中中文标点前后的空格呢？

Before:

english ， english。

Expected:

english，english。

谢谢

Edit:

Here is the English translation according to Google Translate.

When practicing Chinese copywriting, it is easy to make more spaces. Can I use Linter to delete the spaces before and after the Chinese punctuation in the following example?

Before:

english , english.

Expected:

english, english.

thanks
FR: Allow Removal of Whitespace Before and/or After Characters
## Describe the Bug

**Remove Space around Characters** does not work as described

## How to Reproduce


``` 
when I type in "This is a fullwidth comma	，  with text after it."
Instead of getting "This is a fullwidth comma，with text after it."
I got "This is a fullwidth comma	， with text after it." (even after lint the file multiple times)
I also found it does not remove space before the character such as "a word , another word" 
I have it switched on <img width="792" alt="image" src="https://user-images.githubusercontent.com/7776181/213778443-90b92a0c-a249-443a-9c9b-3119d4a8f113.png">

```

Please make sure to include logs and the rule(s) that cause the issue if applicable. You can include logs by going to the debug tab of the Linter and setting the log level to debug. Then enable the capturing of logs and either lint on save or lint the current file. Then go back to the debug tab and you can grab Linter logs from the last run.

If you are not sure which rule(s) may cause the issue, please attach the `data.json` file located at `<vault>/.obsidian/plugins/obsidian-linter/` or copy the config from the debug tab.

## Expected Behavior

It consistently failed to remove space in front of characters such as "," and ".". 

Expected output if applicable:

``` markdown
expected output here
```

## Screenshots

If applicable, add screenshots to help explain your problem.

## Device

- [x] Desktop
- [ ] Mobile

## Additional Context

Add any other context about the problem here.


Pull request description:
Add Rule for Removing Space Either Before or After the Specified Characters
Fixes #593 
Fixes #257 

Went ahead and added a rule that allows for removing space before and or after the specified characters. This is meant to be used for punctuation, but can be used for other characters as well. 
**Do not use this for `{` or `}` as ignored sections of the logic show up as `{TEXT_HERE}` which means they will have whitespace removed from either before or after them depending on how they are added to the space removal lists.** 

Changes Made:
- Added a rule which removes spaces before one list of specified characters and after another list of specified characters
- Added tests and logic to make sure tasks were not being affected when used normally and as subtasks
- Added translations for the new rule in the existing languages.


Patch:
diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts
index 05dcd4b..1b8bc30 100644
--- a/src/lang/locale/de.ts
+++ b/src/lang/locale/de.ts
@@ -539,6 +539,19 @@ export default {
         'description': 'Andere Symbole, die enthalten sind',
       },
     },
+    // remove-space-before-or-after-characters.ts
+    'remove-space-before-or-after-characters': {
+      'name': 'Entfernen Sie Leerzeichen vor oder nach Zeichen',
+      'description': 'Entfernt Leerzeichen vor und nach den angegebenen Zeichen. Beachten Sie, dass dies in einigen Fällen zu Problemen mit dem Markdown-Format führen kann.',
+      'characters-to-remove-space-before': {
+        'name': 'Leerzeichen vor Zeichen entfernen',
+        'description': 'Entfernt Leerzeichen vor den angegebenen Zeichen. **Hinweis: Die Verwendung von `{` oder `}` in der Zeichenliste wirkt sich unerwartet auf Dateien aus, da es in der Ignoriersyntax hinter den Kulissen verwendet wird.**',
+      },
+      'characters-to-remove-space-after': {
+        'name': 'Leerzeichen nach Zeichen entfernen',
+        'description': 'Entfernt Leerzeichen vor den angegebenen Zeichen. **Hinweis: Die Verwendung von `{` oder `}` in der Zeichenliste wirkt sich unerwartet auf Dateien aus, da es in der Ignoriersyntax hinter den Kulissen verwendet wird.**',
+      },
+    },
     // remove-trailing-punctuation-in-heading.ts
     'remove-trailing-punctuation-in-heading': {
       'name': 'Entfernen Sie nachgestellte Satzzeichen in der Überschrift',
diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts
index 65f1856..e71481e 100644
--- a/src/lang/locale/en.ts
+++ b/src/lang/locale/en.ts
@@ -539,6 +539,19 @@ export default {
         'description': 'Other symbols to include',
       },
     },
+    // remove-space-before-or-after-characters.ts
+    'remove-space-before-or-after-characters': {
+      'name': 'Remove Space Before or After Characters',
+      'description': 'Removes space before the specified characters and after the specified characters. Note that this may causes issues with markdown format in some cases.',
+      'characters-to-remove-space-before': {
+        'name': 'Remove Space Before Characters',
+        'description': 'Removes space before the specified characters. **Note: using `{` or `}` in the list of characters will unexpectedly affect files as it is used in the ignore syntax behind the scenes.**',
+      },
+      'characters-to-remove-space-after': {
+        'name': 'Remove Space After Characters',
+        'description': 'Removes space after the specified characters. **Note: using `{` or `}` in the list of characters will unexpectedly affect files as it is used in the ignore syntax behind the scenes.**',
+      },
+    },
     // remove-trailing-punctuation-in-heading.ts
     'remove-trailing-punctuation-in-heading': {
       'name': 'Remove Trailing Punctuation in Heading',
diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts
index bee8577..8e6380e 100644
--- a/src/lang/locale/es.ts
+++ b/src/lang/locale/es.ts
@@ -462,6 +462,19 @@ export default {
         'description': 'Otros símbolos para incluir',
       },
     },
+    // remove-space-before-or-after-characters.ts
+    'remove-space-before-or-after-characters': {
+      'name': 'Quitar el espacio antes o después de los caracteres',
+      'description': 'Elimina el espacio antes de los caracteres especificados y después de los caracteres especificados. Tenga en cuenta que esto puede causar problemas con el formato de descuento en algunos casos.',
+      'characters-to-remove-space-before': {
+        'name': 'Eliminar espacio antes de los caracteres',
+        'description': 'Elimina el espacio antes de los caracteres especificados. **Nota: el uso de `{` o `}` en la lista de caracteres afectará inesperadamente a los archivos, ya que se usa en la sintaxis de ignorar en segundo plano.**',
+      },
+      'characters-to-remove-space-after': {
+        'name': 'Eliminar espacio después de los caracteres',
+        'description': 'Elimina el espacio después de los caracteres especificados. **Nota: el uso de `{` o `}` en la lista de caracteres afectará inesperadamente a los archivos, ya que se usa en la sintaxis de ignorar en segundo plano.**',
+      },
+    },
     'remove-trailing-punctuation-in-heading': {
       'name': 'Eliminar la puntuación final en el encabezado',
       'description': 'Elimina la puntuación especificada al final de los encabezados, asegurándose de ignorar el punto y coma al final de [referencias de entidades de HTML](https://es.wikipedia.org/wiki/Anexo:Referencias_a_entidades_de_caracteres_XML_y_HTML).',
diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts
index 5765adc..693e175 100644
--- a/src/lang/locale/zh-cn.ts
+++ b/src/lang/locale/zh-cn.ts
@@ -539,6 +539,19 @@ export default {
         'description': '要包括的其他符号',
       },
     },
+    // remove-space-before-or-after-characters.ts
+    'remove-space-before-or-after-characters': {
+      'name': '删除字符前后的空格',
+      'description': '删除指定字符之前和指定字符之后的空格。 请注意，在某些情况下，这可能会导致降价格式出现问题。',
+      'characters-to-remove-space-before': {
+        'name': '删除字符前的空格',
+        'description': '删除指定字符前的空格。 **注意：在字符列表中使用`{`或`}`会意外影响文件，因为它在幕后的忽略语法中使用。**',
+      },
+      'characters-to-remove-space-after': {
+        'name': '删除字符后的空格',
+        'description': '删除指定字符后的空格。 **注意：在字符列表中使用`{`或`}`会意外影响文件，因为它在幕后的忽略语法中使用。**',
+      },
+    },
     // remove-trailing-punctuation-in-heading.ts
     'remove-trailing-punctuation-in-heading': {
       'name': '移除标题中的结尾标点符号',
diff --git a/src/rules/remove-space-before-or-after-characters.ts b/src/rules/remove-space-before-or-after-characters.ts
new file mode 100644
index 0000000..9452647
--- /dev/null
+++ b/src/rules/remove-space-before-or-after-characters.ts
@@ -0,0 +1,90 @@
+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 {updateListItemText} from '../utils/mdast';
+import {escapeRegExp} from '../utils/regex';
+
+class RemoveSpaceBeforeOrAfterCharactersOptions implements Options {
+  charactersToRemoveSpacesBefore: string = ',!?;:).’”]';
+  charactersToRemoveSpacesAfter: string = '¿¡‘“([';
+}
+
+@RuleBuilder.register
+export default class RemoveSpaceBeforeOrAfterCharacters extends RuleBuilder<RemoveSpaceBeforeOrAfterCharactersOptions> {
+  constructor() {
+    super({
+      nameKey: 'rules.remove-space-before-or-after-characters.name',
+      descriptionKey: 'rules.remove-space-before-or-after-characters.description',
+      type: RuleType.SPACING,
+    });
+  }
+  get OptionsClass(): new () => RemoveSpaceBeforeOrAfterCharactersOptions {
+    return RemoveSpaceBeforeOrAfterCharactersOptions;
+  }
+  apply(text: string, options: RemoveSpaceBeforeOrAfterCharactersOptions): string {
+    const symbolsBefore = escapeRegExp(options.charactersToRemoveSpacesBefore);
+    const symbolsAfter = escapeRegExp(options.charactersToRemoveSpacesAfter);
+
+
+    if (!symbolsBefore && !symbolsAfter) {
+      return text;
+    }
+
+    const removeWhitespaceBeforeCharacters = new RegExp(`([ \t])+([${symbolsBefore}])`, 'g');
+    const removeWhitespaceAfterCharacters = new RegExp(`([${symbolsAfter}])([ \t])+`, 'g');
+
+    const replaceWhitespaceBeforeOrAfterCharacters = function(text: string): string {
+      return text.replace(removeWhitespaceBeforeCharacters, '$2').replace(removeWhitespaceAfterCharacters, '$1');
+    };
+
+    let newText = ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.math, IgnoreTypes.yaml, IgnoreTypes.link, IgnoreTypes.wikiLink, IgnoreTypes.tag, IgnoreTypes.list, IgnoreTypes.html], text, replaceWhitespaceBeforeOrAfterCharacters);
+
+    newText = updateListItemText(newText, replaceWhitespaceBeforeOrAfterCharacters);
+
+    return newText;
+  }
+  get exampleBuilders(): ExampleBuilder<RemoveSpaceBeforeOrAfterCharactersOptions>[] {
+    return [
+      new ExampleBuilder({
+        description: 'Remove Spaces and Tabs Before and After Default Symbol Set',
+        before: dedent`
+          In the end , the space gets removed\t .
+          The space before the question mark was removed right ?
+          The space before the exclamation point gets removed !
+          A semicolon ; and colon : have spaces removed before them
+          ‘ Text in single quotes ’
+          “ Text in double quotes ”
+          [ Text in square braces ]
+          ( Text in parenthesis )
+        `,
+        after: dedent`
+          In the end, the space gets removed.
+          The space before the question mark was removed right?
+          The space before the exclamation point gets removed!
+          A semicolon; and colon: have spaces removed before them
+          ‘Text in single quotes’
+          “Text in double quotes”
+          [Text in square braces]
+          (Text in parenthesis)
+        `,
+      }),
+    ];
+  }
+  get optionBuilders(): OptionBuilderBase<RemoveSpaceBeforeOrAfterCharactersOptions>[] {
+    return [
+      new TextOptionBuilder({
+        nameKey: 'rules.remove-space-before-or-after-characters.characters-to-remove-space-before.name',
+        descriptionKey: 'rules.remove-space-before-or-after-characters.characters-to-remove-space-before.description',
+        OptionsClass: RemoveSpaceBeforeOrAfterCharactersOptions,
+        optionsKey: 'charactersToRemoveSpacesBefore',
+      }),
+      new TextOptionBuilder({
+        nameKey: 'rules.remove-space-before-or-after-characters.characters-to-remove-space-after.name',
+        descriptionKey: 'rules.remove-space-before-or-after-characters.characters-to-remove-space-after.description',
+        OptionsClass: RemoveSpaceBeforeOrAfterCharactersOptions,
+        optionsKey: 'charactersToRemoveSpacesAfter',
+      }),
+    ];
+  }
+}
diff --git a/src/utils/mdast.ts b/src/utils/mdast.ts
index e8343d7..4a35640 100644
--- a/src/utils/mdast.ts
+++ b/src/utils/mdast.ts
@@ -504,7 +504,14 @@ export function updateListItemText(text: string, func:(text: string) => string):
   const positions: Position[] = getPositions(MDAstTypes.ListItem, text);
 
   for (const position of positions) {
+    let indicatorOffset = 2;
     let listText = text.substring(position.start.offset+2, position.end.offset);
+    const checklistIndicatorRegex = /^\[.\] /;
+    if (checklistIndicatorRegex.test(listText)) {
+      indicatorOffset += 4;
+      listText = listText.substring(4);
+    }
+
     // This helps account for a weird scenario where list items is pulling back multiple list items in one go sometimes
     const listIndicatorRegex = /\n(( |\t)*>?)*(\*|-|\+|- \[( | x)\]|\d+\.) /g;
     const matches = listText.match(listIndicatorRegex);
@@ -516,11 +523,17 @@ export function updateListItemText(text: string, func:(text: string) => string):
       let index = 0;
       // eslint-disable-next-line guard-for-in
       for (const listItem of listItems) {
+        let listItemText = listItem;
         if (index > 0) {
           newListText += matches[index - 1];
         }
 
-        newListText += func(listItem);
+        if (checklistIndicatorRegex.test(listItemText)) {
+          newListText += listItemText.substring(0, 4);
+          listItemText = listItemText.substring(4);
+        }
+
+        newListText += func(listItemText);
         index++;
       }
 
@@ -529,7 +542,7 @@ export function updateListItemText(text: string, func:(text: string) => string):
       listText = func(listText);
     }
 
-    text = replaceTextBetweenStartAndEndWithNewValue(text, position.start.offset+2, position.end.offset, listText);
+    text = replaceTextBetweenStartAndEndWithNewValue(text, position.start.offset+indicatorOffset, position.end.offset, listText);
   }
 
   return text;


Test patch:
diff --git a/__tests__/remove-space-before-or-after-characters.test.ts b/__tests__/remove-space-before-or-after-characters.test.ts
new file mode 100644
index 0000000..292f195
--- /dev/null
+++ b/__tests__/remove-space-before-or-after-characters.test.ts
@@ -0,0 +1,52 @@
+import RemoveSpaceBeforeOrAfterCharacters from '../src/rules/remove-space-before-or-after-characters';
+import dedent from 'ts-dedent';
+import {ruleTest} from './common';
+
+ruleTest({
+  RuleBuilderClass: RemoveSpaceBeforeOrAfterCharacters,
+  testCases: [
+    {
+      testName: 'Make sure that checklists completion indicator does not get affected by the rule',
+      before: dedent`
+        # Title
+        ${''}
+        - [ ] ]Task 1 starting with opening square brace keeps initial space
+        - [ ] Task 2
+        - [x] Task 3
+        - [ ] Task 4 .
+        ${''}
+      `,
+      after: dedent`
+        # Title
+        ${''}
+        - [ ] ]Task 1 starting with opening square brace keeps initial space
+        - [ ] Task 2
+        - [x] Task 3
+        - [ ] Task 4.
+        ${''}
+      `,
+    },
+    {
+      testName: 'Make sure that checklists completion indicator does not get affected by the rule when there is a sublist',
+      before: dedent`
+        # Title
+        ${''}
+        - [ ] Task 1
+          - [ ] Task 2
+          - [x] Task 3 ,
+        - [ ] Task 4 .
+        ${''}
+      `,
+      after: dedent`
+        # Title
+        ${''}
+        - [ ] Task 1
+          - [ ] Task 2
+          - [x] Task 3,
+        - [ ] Task 4.
+        ${''}
+      `,
+    },
+
+  ],
+});