OutSystemsのRelease NotesをCSVに変換する

少し前に、OutSystems 11の特定バージョン間の変更点リストを取得したいことがあった。
Release Notesのページを見れば良いのだが、バージョン間が離れていると、手動でリストを取ってくるのも面倒くさい。
今後のために、簡単なJavaScriptコードのスニペットを用意してみた。

# 対象
OutSystems 11のPlatform Serverでテスト。
ページ構造は変わる可能性もあるので注意。この記事を書いているのは、2025/06/07。

# Release Notesページの構造をチェック
[Release Notes > 11 > Platform Server](https://success.outsystems.com/support/release_notes/11/platform_server/)のタグ構造を確認してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/92e05243-4dc5-4a96-b1b1-95c921a5653a.png)

↑はブラウザの開発者ツールでタグ構造を抜粋したもの。
- data属性data-block=WebBlock.InjectHTMLが親タグになっていそう
- 親タグ直下のタグのさらに下に必要なタグが（階層構造を造らずに）平置きされていそう
- バージョン番号がh2タグ内に含まれる
- h2タグの後のh3タグは、該当バージョン内での修正点のカテゴリ（例：Bug Fixing）
- h3タグの後のリスト（ul > liタグ）は該当カテゴリに属する修正点のリストを表す

# Release Notesの修正点をJavaScriptオブジェクトに変換するJavaScriptコードスニペット
以上の分析結果をもとに、JavaScriptでHTML情報を抜き出すコードを書けば良いが、利便性のためにいくらか検討が必要。
## 考慮点
### カテゴリがNewの場合
カテゴリがNew（新機能）の場合だけ、カテゴリ名にバージョン番号が入ってしまう。
例：New in Platform Server 11.35.0、New in Platform Server 11.34.1など

カテゴリを抽出した際に、「New」で始まっていたら「New」をカテゴリとして利用すれば良さそう。

### カテゴリに揺らぎがある
カテゴリの部分だけを抽出し、重複を排除してJavaScript配列に格納してみると、以下のようになった（Newについては上記対応済み）。

``` javascript
['New', 'Bug Fixing', 'Known issues', 'Breaking change', 'Known Issues', 'Limitation', 'Known issue', 'Breaking Change', 'Breaking changes', 'Breaking Changes']
```

この結果から、
- 'Known issues', 'Known Issues', 'Known issue'　-> 'Known Issues'
- 'Breaking change', 'Breaking Change', 'Breaking changes', 'Breaking Changes' -> 'Breaking Changes'
- 
とカテゴリ名の揺らぎを補正する。

### 更新内容に含まれる二重引用符の対応
最終的にCSVで出力するので、各項目は二重引用符で囲まれる。
その際に、Release Notes上の更新内容がすでに二重引用符を含んでいると、Excelなどで表示したときに表示が壊れてしまう。

そこで、二重引用符は二重にする（"data would be truncated"のような文字列があったら、""data would be truncated""に置換する）。

## コードスニペット
``` javascript
// Get the target tags from the document
let targetTags = document.querySelector("[data-block='WebBlock.InjectHMTL']").childNodes[0].children;

const result = [];
let currentVersion = null;

Array.from(targetTags).forEach(node => {
  if (node.tagName === "H2") {
    if (currentVersion) {
      delete currentVersion._currentCategory;
      result.push(currentVersion);
    }
    currentVersion = { VersionNo: node.innerText, categories: {} };
  } else if (node.tagName === "H3" && currentVersion) {
    currentVersion._currentCategory = node.innerText;
    // If the _currentCategory starts with "New", use just "New" as the category name
    if (currentVersion._currentCategory.startsWith("New")) {
      currentVersion._currentCategory = "New";
    }
    // ['New', 'Bug Fixing', 'Known issues', 'Breaking change', 'Known Issues', 'Limitation', 'Known issue', 'Breaking Change', 'Breaking changes', 'Breaking Changes'] -> Correct fluctuation ['New', 'Bug Fixing', 'Known Issues', 'Breaking Change', 'Limitation']
    if (currentVersion._currentCategory === "Bug Fixing") {
      currentVersion._currentCategory = "Bug Fixing";
    } else if (currentVersion._currentCategory === "Known issues" || currentVersion._currentCategory === "Known issue") {
      currentVersion._currentCategory = "Known Issues";
    } else if (currentVersion._currentCategory === "Breaking change" || currentVersion._currentCategory === "Breaking changes" || currentVersion._currentCategory === "Breaking Change") {
      currentVersion._currentCategory = "Breaking Changes";
    } else if (currentVersion._currentCategory === "Limitation") {
      currentVersion._currentCategory = "Limitation";
    }

    currentVersion.categories[currentVersion._currentCategory] = [];
  } else if (node.tagName === "UL" && currentVersion && currentVersion._currentCategory) {
    const items = Array.from(node.querySelectorAll("li")).map(li => li.innerText.trim().replace(/"/g, '""')); // replace " with ""
    currentVersion.categories[currentVersion._currentCategory].push(...items);
  }
});
if (currentVersion) {
  delete currentVersion._currentCategory;
  result.push(currentVersion);
}

console.log(result);
```

## 実行方法
1. Release Notesのページ（OutSystems11のPlatform Server) をブラウザで開く
1. 開発者ツールを起動
1. コンソールを開く
1. 上記コードスニペットを貼り付けて実行
1. resultという変数にJavaScriptオブジェクトとして格納される

## 実行結果の例
resultは配列。その各要素は、バージョンを表すオブジェクト。
バージョンオブジェクト
- VersionNo: バージョン番号
- categories: オブジェクト
    - カテゴリ名プロパティの値として、更新内容を配列で持つ
    - 例：categories['New'][0]='Added support for .NET Framework 4.8.1. (R11BRT-1152)'
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/25559223-d1ec-4ead-a3a7-9eaff4d407ba.png)

# 結果のJavaScriptオブジェクトをCSV文字列に変換するコードスニペット
## コードスニペット
上のコードに引き続き以下のコードスニペットを、開発者ツールのコンソールで実行する。
``` javascript
const csvRows = [];
// Add header row
csvRows.push("VersionNo,Category,Update");
// Iterate over each version
result.forEach(version => {
  const versionNo = version.VersionNo;
  // Iterate over each category in the version
  Object.keys(version.categories).forEach(category => {
    const updates = version.categories[category];
    updates.forEach(update => {
      csvRows.push(`"${versionNo}","${category}","${update}"`);
    });
  });
});
// Join all rows into a single CSV string
const csvString = csvRows.join("\n");
console.log(csvString);
```

## 実行結果の例
結果をCSVファイルに貼り付け、MacのNumbersアプリで開いたのが以下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/aead3852-9112-4343-b5da-d3418d0ab48f.png)