📑

【C#】Notionからコピーした文章を、読みやすいプレーンテキストに整形する

2022/10/15に公開

はじめに

TL;DR

  • Notionでマークダウンで書いたドキュメントをコピーしSlackなどのメッセージサービスなどにプレーンテキストとしてペーストする際に、マークダウンからうまく変換できずに読みにくくなってしまう問題を解決する
  • ついでにstringの値型?参照型?の実験もした

対象読者

  • Notionでドキュメントを書き、プレーンテキストとしてペーストする人
  • C#の実行環境がある人

環境

  • .NET 6.0.9

背景

問題

  • NotionのマークダウンをSlackなどにペーストする際、そのままコピーしてプレーンテキストとしてではなくペーストする場合にはそれなりに良い。
  • しかし、(コンパクトに表示することなどを目的として)コードブロックに入れるとだいぶ読みにくく崩れる。
  • また、.txt形式のファイルにペーストする場合も同様


Notion


Slackにそのままペースト


Slackのコードブロック内にペースト

  • プレーンテキストの実物はこちら
# H1の見出しです

## H2の見出しです

- リストA
- リストB
    - リストB-1(並列にMeta QuestのURLでブックマークを作成します)
    - 
    
    [Meta Quest VRヘッドセット、アクセサリー、機器 | Meta Quest](https://www.meta.com/jp/quest/)
    
- リストC(並列にPICOのURLでブックマークを作成します)
- 

[PICO Official Website](https://www.picoxr.com/)

- リストD(1階層下にValve IndexのURLをそのまま貼り付けます)
    - [https://store.steampowered.com/sub/354231/?l=japanese](https://store.steampowered.com/sub/354231/?l=japanese)

## H2の見出しです

1. リスト1
2. リスト2
3. リスト3には**太字のテキスト**をくっつけます。
  • 具体的には以下のような問題が起きている
    • #の見出しの下の空行が不要
    • ブックマーク形式でNotionにペーストしたURLが2行改行(Meta QuestとPicoのURL)
    • URLをそのまま貼り付けた部分が、文字列部分とURL部分に分けられており、このままクリックすると404エラーとなる(Valve IndexのURL)
    • インデントが半角スペース4つで大きすぎる

モチベーション

  • ちょっとググったが、良い感じのペースト方法が見当たらなかった
  • せっかくなのでC#で書いてみよう

整形コード

using System.Text;
using System.Text.RegularExpressions;

internal class NotionDocument
{
    static void Main()
    {
        using (var writer = new StreamWriter(@"ファイルパス"))
        using (var reader = new StreamReader(@"ファイルパス"))
        {
            StringBuilder stringBuilder = new StringBuilder(reader.ReadToEnd());

            stringBuilder.Replace("\r\n        \r\n        [", "[")  // URLのブックマーク形式の隙間を修正(以下3行)
                .Replace("\r\n    \r\n    [", "[")  //同上
                .Replace("\r\n\r\n[", "[") //同上
                .Replace("    ", "  "); // インデントのスペース4つ分が広いのでを2つ分に減らす


            // 以下はstring型で処理
            string outputString = "";
            string str = stringBuilder.ToString();

            // ★で利用する変数
            var stringUrlRgx = new Regex(@"\[http.+\]\(");

            string[] lines = str.Split("\r\n");
            foreach (var line in lines)
            {

                // 見出し(#で始まるH1等)の後に空行が空くのを消す
                if (line.StartsWith("#"))
                {
                    outputString += line;
                    continue;
                }

                // ★ URLをそのままはりつける(文字列として)ときに、タイトルを消す
                // [https://~~~~](https://~~~~) → https://~~~~
                var stringUrlMatch = stringUrlRgx.Match(line);
                if (stringUrlMatch.Success)
                {
                    string deleteString = stringUrlMatch.Value;
                    outputString += line.Remove(line.Length - 1).Replace(deleteString, "") + "\r\n";
                    continue;
                }
                    
                outputString += line + "\r\n";
                    
            }

            // 出力
            writer.WriteLine(outputString.ToString());
        }
    }
}

捕捉(string型は値型か参照型か?)

  • string型とStringBuilder型を利用した背景には、値型なのか参照型なのかという点にある。
  • string型は参照型だが値型のようなふるまいをするとのこと。(C#の組み込み型の中でstring型とobject型が参照型)
  • StringBuilder型は当たり前に参照型
  • 以下のコードで実験
using System.Text;

internal class Test
{
    static void Main()
    {

        Console.WriteLine("--- int(値型) ---");
        int x = 1;
        int y = x;
        x += 10;
        Console.WriteLine($"x: {x}");
        Console.WriteLine($"y: {y}");


        Console.WriteLine("--- string(参照型だが値型のふるまい) ---");
        string str1 = "aa";
        string str2 = str1;
        string str3 = "cc";

        str1 += "bb";
        str3.Replace("cc", "dd");
            
        Console.WriteLine($"str1: {str1}");
        Console.WriteLine($"str2: {str2}");
        Console.WriteLine($"str3: {str3}");


        Console.WriteLine("--- StringBuilder(参照型) ---");
        StringBuilder sb1 = new StringBuilder("aa");
        StringBuilder sb2 = sb1;
        StringBuilder sb3 = new StringBuilder("cc");
            
        sb1.Append("bb");
        sb3.Replace("cc", "dd");

        Console.WriteLine($"sb1: {sb1}");
        Console.WriteLine($"sb2: {sb2}");
        Console.WriteLine($"sb3: {sb3}");
    }
}
  • 結果
  • intとstringは、yとstr2が更新されていない。これは値型の挙動。一方sb2は参照元が更新されているので当然更新されている(参照型の挙動)
  • str3はReplaceメソッドで置き換えられた値になっていない。これも参照型であれば参照先の値が更新されると思われるが、値型的にReplaceした値を戻り値として返すメソッドになっているためだと思われる。
  • sb3はReplaceメソッドで置き換えられた値になっている。参照型なので参照先の値が変わっているので当たり前。
  • こちらのブログによると原因は以下のよう。

Discussion