LINQPadのhtml言語向けがほしかったので作ってみた

この話はほとんどC#で作ったデスクトップアプリの話になります。

目次

プログラミング言語の勉強するときって・・・

例えば自分の場合、教科書でもサイトでも、ブラウザを最大化表示しつつ、エディタやIDEを小さいサイズに調整して、コード打ち込んで実行して「あ~こんな感じで動くのね。」と動かしながら学んでいくのですが、いちいち新しいソースファイルを作成して保存して実行して、また新しいソースファイルを作成して保存して実行して、、、とやっていくと、どんどんソースファイルが増えていきます。

これがちょっとアレだよね~と思っていました。分かってしまった後、これらはごみファイルでしかありません(悲しい扱い)。言語を学びたいのであって、ソースファイルを作りたいわけではないんですよね(勉強したいときは)。

で、.NET の場合はLINQPadという神様が考えられたアプリがあります。いちいちソースファイルに保存しなくても実行できる!変数をビジュアライズで見れるので構造が理解しやすい!というものです。

唐突にhtml, css, JavaScriptを勉強したかった自分としては、前半のファイル保存せずに実行するアプリが欲しかったのでした(とりあえずはフロントエンドさえ動かせればOKなノリ)。データのビジュアライズはあれば嬉しいけど、JavaScriptにリフレクション的なのってあるのかな?まぁ分からないので無くてもヨシ!でした。

で、普通に勉強しようとするとhtml, css, JavaScript用に3つファイルを作成してメモ帳や任意のエディタで開いて、ブラウザを開いて、htmlファイルを読み込ませないといけません。4アプリが必要になるのです。

これを、1アプリ内で完結させたい、ソースファイルは保存したくない、というイヤイヤ期になりました(赤ちゃんか!)。

探したけど見つけられなかった・・・

なんかないかなぁとネットを探してみましたが探せませんでしたorz。

作ったろうやないかい!

見つかるまで探すマンにはなれず、もう作った方が速くねマンだったので(面倒くさかっただけかも)作りました。ここからは3分クッキングアプリです。

  1. C#/WPFプロジェクトを作成します(確か.NETFramework は4.5以上の方が良かったような?)。
  2. アーキテクチャx86に切り替えます(AvalonDock, CefSharp を動かすため)
  3. NuGetで以下を取ってきます。
    1. AvalonDock
    2. AvalonEdit
    3. CefSharp(Chromiumブラウザを使ってプレビューさせる)
  4. xaml, コードビハインドを書いて、はい出来上がり!

あらかじめ1-3まで用意したものを準備しておきます

html, css, JavaScriptシンタックスハイライトをしたかったためAvalonEdit, 配置を好き勝手に変えたくなると思ったのでAvalonDock, IEではなくChrome 系が良いなと思ったのでCefSharpです。AvalonEditはインテリセンス補完も欲しかったのですが、調査が必要っぽかったのであきらめました(強力機能よりも最速のシンプル完成を優先しました)。

ではここに準備しておいたソリューションに対して、xamlとコードビハインドをコピペしましょう!以下のソースでは、プロジェクト名をJSPadとか格好つけていますが、任意の名前空間に合わせて変えてください。

htmlの勉強を早くしたかったし、完成できればいいやと思っていたので、MVVMではなくコードビハインドでちゃちゃっと作っています。イベントの購読はxaml上ではなく、コントロールに名前を付けておいてコードビハインドで購読しています。

MainWindow.xaml

<Window x:Class="JSPad.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:JSPad"
        xmlns:dock="http://schemas.xceed.com/wpf/xaml/avalondock"
        xmlns:edit="http://icsharpcode.net/sharpdevelop/avalonedit"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <dock:DockingManager>
        <dock:LayoutRoot>
            <dock:LayoutPanel Orientation="Vertical">

                <!-- HTML, CSS, JavaScript ペイン -->
                <dock:LayoutDocumentPane>

                    <dock:LayoutDocument Title="HTML">
                        <edit:TextEditor x:Name="htmlEditor" FontFamily="Consolas" FontSize="16" SyntaxHighlighting="HTML" ShowLineNumbers="True" />
                    </dock:LayoutDocument>

                    <dock:LayoutDocument Title="CSS">
                        <edit:TextEditor x:Name="cssEditor" FontFamily="Consolas" FontSize="16" SyntaxHighlighting="CSS" ShowLineNumbers="True" />
                    </dock:LayoutDocument>

                    <dock:LayoutDocument Title="JavaScript">
                        <DockPanel>
                            <ToolBarTray DockPanel.Dock="Top">
                                <ToolBar>
                                    <Button x:Name="devToolsButton" Content="DevTools(別画面)を表示" />
                                </ToolBar>
                            </ToolBarTray>
                            <edit:TextEditor x:Name="jsEditor" FontFamily="Consolas" FontSize="16" SyntaxHighlighting="JavaScript" ShowLineNumbers="True" />
                        </DockPanel>
                    </dock:LayoutDocument>

                </dock:LayoutDocumentPane>

                <!-- ブラウザプレビュー(ChromiumWebBrowser はコードビハインド上から登録、xaml 上だとデザインが無効になってしまう) -->
                <dock:LayoutAnchorablePane DockHeight="200">
                    <dock:LayoutAnchorable Title="プレビュー">
                        <ContentControl x:Name="browserContainer" />
                    </dock:LayoutAnchorable>
                </dock:LayoutAnchorablePane>

            </dock:LayoutPanel>
        </dock:LayoutRoot>
    </dock:DockingManager>
    
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
//using System.Windows.Shapes; // Path.xxx の名前衝突のため
using System.IO;
using CefSharp.Wpf;
using CefSharp;


// AvalonDock, CefSharp のためにも x86 アーキテクチャがいいかも

namespace JSPad
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        ChromiumWebBrowser browser = null;

        public MainWindow()
        {
            InitializeComponent();
            
            browser = new ChromiumWebBrowser();
            browserContainer.Content = browser;

            htmlEditor.TextChanged += Editor_TextChanged;
            cssEditor.TextChanged += Editor_TextChanged;
            jsEditor.TextChanged += Editor_TextChanged;
            devToolsButton.Click += (s, e) =>
            {
                if (browser.IsBrowserInitialized)
                    browser.ShowDevTools();
            };

            htmlEditor.Text = @"<!DOCTYPE html>
<html lang='ja'>

  <head>
      <meta charset='utf-8'>
      <title>Test Page</title>
  </head>
  
  <body>
      <div id='test'>Hello World!</div>
  </body>

</html>
";
        }

        // 3つのエディタ内容を1つのhtmlソースにマージして表示します。
        private void Editor_TextChanged(object sender, EventArgs e)
        {
            // html に、css と javascript を合体させる
            var html = htmlEditor.Text;
            var css = cssEditor.Text;
            var js = jsEditor.Text;

            // html
            if (!string.IsNullOrWhiteSpace(html))
            {
                // css
                if (!string.IsNullOrWhiteSpace(css))
                {
                    css = $"<style type='text/css'>{css}</style>";
                    html = html.Replace("</head>", $"{css}</head>");
                }

                // javascript
                if (!string.IsNullOrWhiteSpace(js))
                {
                    js = $"<script type='text/javascript'>{js}</script>";
                    html = html.Replace("</body>", $"{js}</body>");
                }
            }
            else
            {
                // html, css が空欄で、javascript がある場合、javascriptが動くように最低限のhtmlを用意して表示させる
                if (string.IsNullOrWhiteSpace(css) && !string.IsNullOrWhiteSpace(js))
                {
                    html = @"<!DOCTYPE html>
<html lang='ja'>

  <head>
      <meta charset='utf-8'>
      <title>Test Page</title>
  </head>
  
  <body>
  </body>

</html>
";

                    js = $"<script type='text/javascript'>{js}</script>";
                    html = html.Replace("</body>", $"{js}</body>");
                }

            }
            
            // 作業フォルダの作成とhtmlファイルの保存
            var appDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "work");
            if (Directory.Exists(appDirectory))
                Directory.Delete(appDirectory, true);
            Directory.CreateDirectory(appDirectory);

            var htmlFile = Path.Combine(appDirectory, "index.html");
            File.WriteAllText(htmlFile, html);

            // ブラウザに表示
            browser.Address = htmlFile;
        }
    }
}

仕様

仕組みとしては、以下のようにcss, javascriptをhtmlにマージさせて、実行ファイルと同じ場所にhtmlソースファイルを作成してブラウザに読み込ませています。

  1. cssの内容をstyleタグで囲って、headの閉じタグ直前に挿入
  2. JavaScriptの内容をscriptタグで囲って、bodyの閉じタグ直前に挿入
  3. 1つのhtmlファイルとしてファイル保存&ブラウザ読み込み

ただし、html, cssが空欄で、JavaScriptのみ空欄ではない場合でも動作させたかったので、この場合は、最低限のhtmlを自動生成して組み込んでいます。

あらかじめ完成したものがこちらの画像です

f:id:sutefu7:20190824003210p:plain

Hello World! に対して、css で文字の色付けが適用されて、その後JavaScriptで表示文字の書き換えが行われました。 f:id:sutefu7:20190824003228p:plain

console.log("xx")などの場合は、DevTools画面を表示させて確認します。 f:id:sutefu7:20190824003239p:plain

JavaScriptのみの場合でも確認できます。 f:id:sutefu7:20190824003251p:plain

結論

全体的にホワイトなので明るすぎるのでダークモードも組み込みたいし、インテリセンスも組み込みたいし、個別保存も対応したいし、TextChanged イベントでやりくりするのはちょっと厳しいかもね~とか思いながらも、後でいいやと思ってしまっていたり。