【JavaScript】サーバ不要 クライアントサイドWiki(PukiWiki風)

プログラミング・Web開発カテゴリを表すパンダのイラスト プログラミング・Web開発

サーバーを構築できないローカル環境向けに、HTMLファイルとJavaScript(JScript)のみで動作するPukiWiki風のクライアントサイドWikiスクリプトを作成した際の記録です。

※ 動作環境についての注意
このスクリプトは ActiveXObject("Scripting.FileSystemObject") を使用してローカルファイルの読み書きを行っています。そのため、ChromeやEdge、Firefoxなどのモダンブラウザでは動作しません。旧Internet Explorer環境やHTML Application(.hta)として実行することを前提としたレガシーコードです。

ソースコード (index.html)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>クライアントサイドWiki</title>
<script>
// グローバル変数を設定
var fso = new ActiveXObject("Scripting.FileSystemObject");
var oBaseFolder = getBaseFolder();

// このHTMLファイルが置かれているフォルダのパス名を取得する
function getBaseFolder() {
    var FullPath = location.href.replace(/file:\/\/\//, "").replace(/file:\/\//, "//").replace(/%20/g, " ");
    if (fso.fileExists(FullPath)) {
        var thisFile = fso.getFile(FullPath);
        return thisFile.parentFolder;
    } else {
        alert('File not Exists:' + FullPath);
    }
}

// ページ名を指定してページを開く
function open(pagename) {
    var content = getContent(pagename);
    if (!content) {
        return edit(pagename);
    }

    // 画像を解析 [[画像名]]
    var pattern = /\[\[([-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+\.(gif|jpe?g|png|bmp))\]\]/ig;
    content = content.replace(pattern, "<img src='$1'>");

    // URLの解析1 エイリアス[[エイリアス名>URL]]
    content = content.replace(/\[\[(\S+?)>(\S+?)\]\]/ig, '<a href="$2" target="_blank">$1</a>');

    // URLの解析2 [[URL]]
    content = content.replace(/\[\[((http|https):\/\/.+)\]\]/ig, '<a href="$1" target="_blank">$1</a>');

    // UNCパスの解析 [[エイリアス名>\\サーバー名\XXX]]
    content = content.replace(/\[\[(\S+?)>(\\\\.+)\]\]/ig, '<a href="$2" target="_blank">$1</a>');

    // UNCパスの解析 [[\\サーバー名\XXX]]
    content = content.replace(/\[\[(\\\\.+)\]\]/ig, '<a href="$1" target="_blank">$1</a>');

    // PukiWiki記法の装飾解析
    content = content.replace(/COLOR\(([^\(\)]*)\){([^}]*)}/g, '<span style="color:$1">$2</span>');
    content = content.replace(/SIZE\(([^\(\)]*)\){([^}]*)}/g, '<span style="font-size:$1px">$2</span>');
    content = content.replace(/COLOR\(([^\(\)]*)\):((?:(?!COLOR\([^\)]+\):).)*)/ig, '<span style="color:$1">$2</span>');
    content = content.replace(/SIZE\(([^\(\)]*)\):((?:(?!SIZE\([^\)]+\):).)*)/ig, '<span class="size$1">$2</span>');
    content = content.replace(/%%%(?!%)((?:(?!%%%).)*)%%%?/ig, '<ins>$1</ins>');
    content = content.replace(/%%(?!%)((?:(?!%%).)*)%%/g, '<del>$1</del>');
    content = content.replace(/'''(?!')((?:(?!''').)*)'''/g, '<em>$1</em>');
    content = content.replace(/''(?!')((?:(?!'').)*)''/g, '<strong>$1</strong>');

    // 内部リンクを開く
    var html = '<pre>' + content.replace(/\[\[([^\]]+)\]\]/g, "<a href='javascript:open(\"$1\")'>$1</a>") + '</pre>';

    setPageName(pagename);
    setContent(html);
    showEditLink();
}

// ページ名を指定してページを編集する
function edit(pagename) {
    var content = getContent(pagename);
    var html = '<form onsubmit="save(\'' + pagename + '\'); return false;">' +
        '<textarea cols="120" rows="30" id="textarea" wrap="off">' + content + '</textarea><br />' +
        '<input type="submit" value="保存"><br />' +
        '</form>';

    setPageName(pagename);
    setContent(html);
    hideEditLink();
}

// いま見ているページを編集する
function editCurrentPage() {
    var pagename = id('headerH1').innerText;
    edit(pagename);
}

// フォームのテキストエリアの中身を保存する
function save(pagename) {
    var content = id('textarea').value;
    var filepath = getFilePath(pagename);
    var file = fso.createTextFile(filepath, true);
    file.write(content);
    file.Close();
    open(pagename);
}

// ページ一覧画面を表示する
function openIndexPage() {
    var enuFiles = new Enumerator(oBaseFolder.Files);
    var myFiles = [];
    for (; !enuFiles.atEnd(); enuFiles.moveNext()) {
        var FilePath = enuFiles.item();
        var ExtensionName = fso.GetExtensionName(FilePath);
        var BaseName = fso.GetBaseName(FilePath);
        if (ExtensionName.toLowerCase() === "txt") {
            myFiles.push(BaseName);
        }
    }
    var list = [];
    for (var i = 0; i < myFiles.length; i++) {
        list.push('<li><a href="javascript:open(\'' + myFiles[i] + '\');">' + myFiles[i] + '</a></li>');
    }
    var content = list.join("\n");
    setPageName('ページ一覧');
    setContent('<ul>' + content + '</ul>');
    hideEditLink();
}

// ページ名からテキストファイルのパスを取得する
function getFilePath(pagename) {
    return oBaseFolder.Path + "\\" + pagename + ".txt";
}

// テキストファイルの中身を取得する
function getContent(pagename) {
    var content = '';
    var filepath = getFilePath(pagename);
    if (fso.fileExists(filepath)) {
        var file = fso.openTextFile(filepath, 1);
        if (!file.atEndOfStream) {
            content = file.readAll();
        }
        file.Close();
    }
    return content;
}

function showEditLink() { id('editLink').style.display = "inline"; }
function hideEditLink() { id('editLink').style.display = "none"; }
function setContent(html) { id('content').innerHTML = html; }
function setPageName(pagename) { if (pagename) { id('headerH1').innerText = pagename; } }
function id(s) { return document.getElementById(s); }

// 検索画面を表示する
function FindIndexPage() {
    var enuFiles = new Enumerator(oBaseFolder.Files);
    var myFiles = [];
    var sword = id('Text1').value;
    if (!sword) return;

    for (; !enuFiles.atEnd(); enuFiles.moveNext()) {
        var FilePath = enuFiles.item();
        var ExtensionName = fso.GetExtensionName(FilePath);
        var BaseName = fso.GetBaseName(FilePath);

        if (ExtensionName.toLowerCase() === "txt") {
            if (BaseName.indexOf(sword) >= 0) {
                myFiles.push(BaseName);
            } else if (openAndSearch(BaseName, sword) >= 0) {
                myFiles.push(BaseName);
            }
        }
    }

    var list = [];
    for (var i = 0; i < myFiles.length; i++) {
        list.push('<li><a href="javascript:open(\'' + myFiles[i] + '\');">' + myFiles[i] + '</a></li>');
    }
    var content = list.join("\n");
    setPageName('検索結果: ' + sword);
    setContent('<ul>' + content + '</ul>');
    hideEditLink();
}

function openAndSearch(pagename, sword) {
    var content = getContent(pagename);
    if (!content) return -1;
    return content.indexOf(sword);
}
</script>
<style>
    #Text1 { width: 120px; }
    .style1 { width: 50%; }
</style>
</head>
<body onload="open('トップページ')">
    <table width="100%" border="0">
        <tr valign="top">
            <td class="style1">
                <h1 id="headerH1">トップページ</h1>
            </td>
            <td align="right">
                <a href="javascript:open('トップページ')">トップページ</a> |
                <span id="editLink"><a href="javascript:editCurrentPage()">編集</a> | </span>
                <a href="javascript:openIndexPage()">ページ一覧</a> |
                <input id="Text1" type="text" />
                <a href="javascript:FindIndexPage()">検索</a>
            </td>
        </tr>
    </table>
    <hr />
    <div id="content"></div>
</body>
</html>

この記事の更新履歴

この記事は、生成AIを活用した自動レビュー・更新フローにより内容を見直し、必要な修正を反映しています。

2026年9月11日

  • 追加ActiveXObject(FileSystemObject)を使用しているため、Internet ExplorerやHTA等のレガシー環境専用である旨の注意喚起を追加しました。
  • 変更HTMLタグが混入して破損していた正規表現やコードブロックの体裁を正しく整形しました。
  • 削除リンク切れとなっている外部ファイルのダウンロードリンクを整理しました。

文書情報

記事タイトル
【JavaScript】サーバ不要 クライアントサイドWiki(PukiWiki風)
作成日
更新日
Source URL
https://papanda925.com/?p=430

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

タイトルとURLをコピーしました