Procházet zdrojové kódy

Merge pull request #9690 from weseek/feat/normalize-remark-growi-directives

feat: Normalize remark growi directives for v6.0.x or above
Yuki Takei před 1 rokem
rodič
revize
fcbcd760ff

+ 3 - 2
bin/data-migrations/README.md

@@ -8,8 +8,9 @@
 git clone https://github.com/weseek/growi
 cd growi/bin/data-migrations
 
-NETWORK=growi_devcontainer_default \
-MONGO_URI=mongodb://growi_devcontainer_mongo_1/growi \
+NETWORK=growi_devcontainer_default
+MONGO_URI=mongodb://growi_devcontainer_mongo_1/growi
+
 docker run --rm \
   --network $NETWORK \
   -v "$(pwd)"/src:/opt \

+ 2 - 1
bin/data-migrations/src/migrations/v60x/index.js

@@ -2,6 +2,7 @@ const bracketlink = require('./bracketlink');
 const csv = require('./csv');
 const drawio = require('./drawio');
 const plantUML = require('./plantuml');
+const remarkGrowiDirective = require('./remark-growi-directive');
 const tsv = require('./tsv');
 
-module.exports = [...bracketlink, ...csv, ...drawio, ...plantUML, ...tsv];
+module.exports = [...bracketlink, ...csv, ...drawio, ...plantUML, ...tsv, ...remarkGrowiDirective];

+ 25 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/README.ja.md

@@ -0,0 +1,25 @@
+# remark-growi-directive
+
+以下の要領で replace する
+
+なお、`$foo()` は一例であり、`$bar()`, `$baz()`, `$foo-2()` など、さまざまな directive に対応する必要がある
+
+## 1. HTMLタグ内で `$foo()` を利用している箇所
+- 置換対象文章の詳細
+  - `$foo()`がHTMLタグ内かつ、当該`$foo()`記述の1行前が空行ではない場合に1行前に空行を挿入する
+  - `$foo()`がHTMLタグ内かつ、`$foo()`記述行の行頭にインデントがついている場合に当該行のインデントを削除する
+  - `$foo()`がHTMLタグ内かつ、当該`$foo()`記述の1行後のHTMLタグ記述行にインデントがついている場合にその行頭のインデントを削除する
+
+## 2. `$foo()` を利用している箇所
+- 置換対象文章の詳細
+  - `$foo()`の引数内で `filter=` あるいは `except=` に対する値に括弧 `()` を使用している場合、括弧を削除する
+    - before: `$foo()`(depth=2, filter=(AAA), except=(BBB))
+    - after: `$foo()`(depth=2, filter=AAA, except=BBB)
+
+## テストについて
+
+以下を満たす
+
+- input が `example.md` のとき、`example-expected.md` を出力する
+- input が `example-expected.md` のとき、`example-expected.md` を出力する (変更が起こらない)
+

+ 43 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/example-expected.md

@@ -0,0 +1,43 @@
+# Should not be replaced
+
+filter=(FOO), except=(word1|word2|word3)
+
+# Should be replaced
+
+<div class="container-fluid">
+    <div class="row">
+        <div>
+            <div>FOO</div>
+
+$foo(depth=2, filter=FOO)
+</div>
+        <div>
+            <div>BAR</div>
+
+$bar(depth=2, filter=BAR)
+</div>
+        <div>
+            <div>BAZ</div>
+
+$baz(depth=2, filter=BAZ)
+</div>
+    </div>
+    <hr>
+    <div class="row">
+        <div>
+            <div>FOO</div>
+
+$foo(depth=2, filter=FOO, except=word1|word2|word3)
+</div>
+        <div>
+            <div>BAR</div>
+
+$bar(depth=2, filter=BAR, except=word1|word2|word3)
+</div>
+        <div>
+                <div>BAZ</div>
+
+$baz(depth=2, filter=BAZ, except=word1|word2|word3)
+</div>
+    </div>
+</div>

+ 37 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/example.md

@@ -0,0 +1,37 @@
+# Should not be replaced
+
+filter=(FOO), except=(word1|word2|word3)
+
+# Should be replaced
+
+<div class="container-fluid">
+    <div class="row">
+        <div>
+            <div>FOO</div>
+            $foo(depth=2, filter=(FOO))
+        </div>
+        <div>
+            <div>BAR</div>
+            $bar(depth=2, filter=(BAR))
+        </div>
+        <div>
+            <div>BAZ</div>
+            $baz(depth=2, filter=(BAZ))
+        </div>
+    </div>
+    <hr>
+    <div class="row">
+        <div>
+            <div>FOO</div>
+            $foo(depth=2, filter=(FOO), except=(word1|word2|word3))
+        </div>
+        <div>
+            <div>BAR</div>
+            $bar(depth=2, filter=(BAR), except=(word1|word2|word3))
+        </div>
+        <div>
+                <div>BAZ</div>
+            $baz(depth=2, filter=(BAZ), except=(word1|word2|word3))
+        </div>
+    </div>
+</div>

+ 1 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/index.js

@@ -0,0 +1 @@
+module.exports = require('./remark-growi-directive');

+ 65 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/remark-growi-directive.js

@@ -0,0 +1,65 @@
+/**
+ * @typedef {import('../../../types').MigrationModule} MigrationModule
+ */
+
+module.exports = [
+  /**
+   * Adjust line breaks and indentation for any directives within HTML tags
+   * @type {MigrationModule}
+   */
+  (body) => {
+    const lines = body.split('\n');
+    const directivePattern = /\$[\w\-_]+\([^)]*\)/;
+    let lastDirectiveLineIndex = -1;
+
+    for (let i = 0; i < lines.length; i++) {
+      if (directivePattern.test(lines[i])) {
+        const currentLine = lines[i];
+        const prevLine = i > 0 ? lines[i - 1] : '';
+        const nextLine = i < lines.length - 1 ? lines[i + 1] : '';
+
+        // Always remove indentation from directive line
+        lines[i] = currentLine.trimStart();
+
+        // Insert empty line only if:
+        // 1. Previous line contains an HTML tag (ends with >)
+        // 2. Previous line is not empty
+        // 3. Previous line is not a directive line
+        const isPrevLineHtmlTag = prevLine.match(/>[^\n]*$/) && !prevLine.match(directivePattern);
+        const isNotAfterDirective = i - 1 !== lastDirectiveLineIndex;
+
+        if (isPrevLineHtmlTag && prevLine.trim() !== '' && isNotAfterDirective) {
+          lines.splice(i, 0, '');
+          i++;
+        }
+
+        // Update the last directive line index
+        lastDirectiveLineIndex = i;
+
+        // Handle next line if it's a closing tag
+        if (nextLine.match(/^\s*<\//)) {
+          lines[i + 1] = nextLine.trimStart();
+        }
+      }
+    }
+
+    return lines.join('\n');
+  },
+
+  /**
+   * Remove unnecessary parentheses in directive arguments
+   * @type {MigrationModule}
+   */
+  (body) => {
+    // Detect and process directive-containing lines in multiline mode
+    return body.replace(/^.*\$[\w\-_]+\([^)]*\).*$/gm, (line) => {
+      // Convert filter=(value) to filter=value
+      let processedLine = line.replace(/filter=\(([^)]+)\)/g, 'filter=$1');
+
+      // Convert except=(value) to except=value
+      processedLine = processedLine.replace(/except=\(([^)]+)\)/g, 'except=$1');
+
+      return processedLine;
+    });
+  },
+];

+ 43 - 0
bin/data-migrations/src/migrations/v60x/remark-growi-directive/remark-growi-directive.spec.js

@@ -0,0 +1,43 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { describe, test, expect } from 'vitest';
+
+import migrations from './remark-growi-directive';
+
+describe('remark-growi-directive migrations', () => {
+  test('should transform example.md to match example-expected.md', () => {
+    const input = fs.readFileSync(path.join(__dirname, 'example.md'), 'utf8');
+    const expected = fs.readFileSync(path.join(__dirname, 'example-expected.md'), 'utf8');
+
+    const result = migrations.reduce((text, migration) => migration(text), input);
+    expect(result).toBe(expected);
+  });
+
+  test('should not modify example-expected.md', () => {
+    const input = fs.readFileSync(path.join(__dirname, 'example-expected.md'), 'utf8');
+
+    const result = migrations.reduce((text, migration) => migration(text), input);
+    expect(result).toBe(input);
+  });
+
+  test('should handle various directive patterns', () => {
+    const input = `
+<div>
+    $foo(filter=(AAA))
+    $bar-2(except=(BBB))
+    $baz_3(filter=(CCC), except=(DDD))
+</div>`;
+
+    const expected = `
+<div>
+
+$foo(filter=AAA)
+$bar-2(except=BBB)
+$baz_3(filter=CCC, except=DDD)
+</div>`;
+
+    const result = migrations.reduce((text, migration) => migration(text), input);
+    expect(result).toBe(expected);
+  });
+});

+ 9 - 0
bin/vitest.config.ts

@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    environment: 'node',
+    clearMocks: true,
+    globals: true,
+  },
+});

+ 1 - 0
vitest.workspace.mts

@@ -1,6 +1,7 @@
 export default [
   'apps/*/vitest.config.ts',
   'apps/*/vitest.workspace.ts',
+  'bin/vitest.config.ts',
   'packages/*/vitest.config.ts',
   'packages/*/vitest.workspace.ts',
 ];