#author("2026-08-31T21:18:10+09:00","default:guide","guide") #author("2026-09-04T00:05:47+09:00","default:mcp","bot-mcp") * Pkript/サンプル [#p8a0b62c] Pkriptスクリプトのサンプル集です。 plugin/pkript/script/<名前>.js に保存するか、:config/pkript/script/<名前> ページに書けば動作します。 #contents ** hello - あいさつ [#y0e52f3f] 引数を受け取ってHTMLを出力する基本的な例です。 plugin/pkript/script/hello.js #highlight{{ function plugin_hello_convert(e) { var args = func_get_args(); var name = args[0] || "World"; return "<p>Hello, " + htmlsc(name) + "!</p>"; } function plugin_hello_inline(e) { var name = e.args[0] || "World"; return "<span>Hello, " + htmlsc(name) + "!</span>"; } }} 使い方: #pkript(hello, World) &pkript(hello, World); 出力: <p>Hello, World!</p> <span>Hello, World!</span> ** badge - 色付きラベル [#r6b1dda0] style属性でインラインバッジを表示します。 plugin/pkript/script/badge.js #pkript(highlight){{ #highlight{{ function plugin_badge_inline(e) { var label = htmlsc(e.args[0] || "NEW"); var color = e.args[1] || "#0066cc"; var style = "display:inline-block; padding:2px 8px; border-radius:4px; " + "background-color:" + color + "; color:#ffffff; font-size:12px"; return "<span style='" + style + "'>" + label + "</span>"; } }} 使い方: &badge(NEW, #cc0000); &badge(v0.3, #0066cc); ** box - メッセージ枠 [#l9762ebb] タイトルと本文を受け取って囲み枠を出力します。 plugin/pkript/script/box.js #pkript(highlight){{ #highlight{{ function plugin_box_convert(e) { var title = htmlsc(e.args[0] || "お知らせ"); var text = htmlsc(e.args[1] || ""); return "<div style='border:1px solid #cccccc; border-radius:4px; padding:10px; margin:8px 0;'>" + "<div style='font-weight:bold; margin-bottom:4px;'>" + title + "</div>" + "<div>" + text + "</div>" + "</div>"; } }} 使い方: #box(お知らせ, 明日はメンテナンスを実施します。) ** sum - 引数の合計 [#h9550a2f] 渡された数値を合算して返します。 plugin/pkript/script/sum.js #pkript(highlight){{ #highlight{{ function plugin_sum_inline(e) { var total = 0; for (const arg of e.args) { total += Number(arg); } return "" + total; } }} 使い方: &sum(10, 20, 30); ** pagelist - ページ一覧 [#ra1c197b] wiki.pages と map メソッドを使って、指定プレフィックスのページ一覧を出力します。 plugin/pkript/script/pagelist.js #pkript(highlight){{ #highlight{{ function plugin_pagelist_convert(e) { var prefix = e.args[0] || ""; var pages = wiki.pages(prefix); if (pages.length == 0) return "<p>該当するページがありません。</p>"; var listItems = pages.map((p) => "<li>" + wiki.link(p) + "</li>").join("\n"); return "<ul>\n" + listItems + "\n</ul>"; } }} 使い方: #pagelist(Docs/) ** guestbook - 投稿フォーム [#oea68337] フォームから入力された内容をページに追記します。 plugin/pkript/script/guestbook.js #pkript(highlight){{ #highlight{{ function plugin_guestbook_convert(e) { return "<form method=\"post\">" + "<input type=\"hidden\" name=\"plugin\" value=\"pkript\">" + "<input type=\"hidden\" name=\"script\" value=\"guestbook\">" + "<input type=\"hidden\" name=\"pkript_token\" value=\"" + wiki.token() + "\">" + "<p>お名前: <input type=\"text\" name=\"name\"></p>" + "<p>コメント: <textarea name=\"comment\" rows=\"3\" cols=\"40\"></textarea></p>" + "<p><input type=\"submit\" value=\"投稿する\"></p>" + "</form>"; } function plugin_guestbook_action(e) { if (e.method != "POST") return "<p>POSTで送信してください。</p>"; var name = e.vars["name"] || "名無し"; var comment = e.vars["comment"] || ""; if (comment == "") return "<p>コメントを入力してください。</p>"; var entry = "\n- " + name + " : " + comment; wiki.append("GuestBook", entry); return "<p>投稿を受け付けました。<a href=\"./?GuestBook\">GuestBookへ戻る</a></p>"; } }} 使い方: #guestbook *** 投稿内容の扱い [#p2121264] wiki.append で書き込んだ内容は''Wiki記法として解釈されます''。投稿者が書いた文字列がそのままページの本文になるので、記法もプラグイン呼び出しも書けてしまいます。 wiki.append で書き込んだ内容はWiki記法として解釈されます。投稿者が書いた文字列がそのままページの本文になるので、記法もプラグイン呼び出しも書けてしまいます。 PukiWiki 本体の #comment も同じ作りですが、誰でも投稿できるページで使うなら、投稿を受け取る側で記法を潰すか、書き込み先を $edit_auth_pages で保護してください。 // 行頭記号と改行を潰してから書き込む例 var comment = e.vars["comment"].replaceAll("\n", " "); if (comment.startsWith("#") || comment.startsWith("*")) comment = " " + comment; ** vote - 投票(データ保存) [#vote_sample] data.* に集計を保存する投票です。同梱の plugin/pkript/script/vote.js がそのまま動きます。 使い方: #vote(好きな色){{ 赤 青 緑 }} 要点だけを抜き出すと次のようになります。 #pkript(highlight){{ #highlight{{ const keyFor = (page, title) => "vote/" + slug(page) + "/" + slug(title); function plugin_vote_convert(e) { const tally = data.get(keyFor(e.page, title), {counts: {}, voters: []}); // ...集計を表に描き、投票フォームを出す } function plugin_vote_action(e) { const key = keyFor(e.vars["page"], e.vars["title"]); const tally = data.get(key, {counts: {}, voters: []}); tally.counts[choice] = countOf(tally, choice) + 1; if (e.user.name != "") tally.voters.push(e.user.name); data.set(key, tally); } }} - 集計は :config/pkript/data/vote/<ページ>/<質問> に入ります。投票するのにページの編集権限は要りません。 - ログインしていれば、投票した人の名前を控えて二重投票を止めます。認証の無いWikiでは止められません。 - フォームを出すかどうかは data.canWrite() で決めます。 ** vote2 - 投票(ページ内保存) [#vote2_sample] PukiWiki本体の vote.inc.php と同じで、集計を''呼び出し行そのもの''に書き戻します。同梱の plugin/pkript/script/vote2.js です。 PukiWiki本体の vote.inc.php と同じで、集計を呼び出し行そのものに書き戻します。同梱の plugin/pkript/script/vote2.js です。 使い方: #vote2(赤[1],あお[0],green) 投票すると、ページのその行が書き換わります。 #vote2(赤[1],あお[1],green[0]) #pkript(highlight){{ #highlight{{ function plugin_vote2_action(e) { const source = wiki.source(page); const line = findCall(source, sent); // 自分の行を探す const current = parseChoices(argsOf(line)); // ページ側の数を読み直す current[index].count = current[index].count + 1; wiki.write(page, source.replace(line, "#vote2(" + formatChoices(current) + ")")); wiki.redirect(page); } }} - 集計がページの中にあるので、差分・バックアップ・凍結がそのまま効きます。 - そのかわり''投票する人にページの編集権限が必要''です。書けないページではボタンを出しません。 - そのかわり投票する人にページの編集権限が必要です。書けないページではボタンを出しません。 - 送信されてきた数ではなく、書き込む直前にページから読み直した数に足します。送信から書き込みまでの間に誰かが投票していても、その票を消しません。 - 書き込みのあと wiki.redirect() でページに戻します。これが無いと、再読み込みでもう一度投票されます。 *** どちらを使うか [#vb3be054] *** どちらを使うか [#m4d184ef] |~項目|~vote(data.*)|~vote2(ページ内)|h |~|~vote(data.*)|~vote2(ページ内)|h |投票に必要な権限|不要|ページの編集権限| |集計の置き場所|:config/pkript/data/*|呼び出し行の中| |差分・バックアップ|データページ側に付く|本文と一緒に付く| |二重投票の抑止|ログイン中なら可能|しない(本体と同じ)| ** jsxcard - JSX記法のカード [#b99f0cab] JSX記法で枠付きのカードを出します。文字列連結を使わずにHTMLを組み立てます。 plugin/pkript/script/jsxcard.js #pkript(highlight){{{ #highlight{{{ // Pkript sample: #pkript(jsxcard, 見出し, class=note){{ // 本文 // }} const COLORS = { note: "#4a6", warn: "#c62", info: "#468" }; function pickColor(kind) { return Object.has(COLORS, kind) ? COLORS[kind] : COLORS["info"]; } function plugin_jsxcard_convert(e) { const title = e.args[0] || "(無題)"; const kind = e.opts["class"] || "info"; const color = pickColor(kind); const lines = e.body == "" ? [] : e.body.split("\n"); return <div class={"card " + kind} style={"border-left: 4px solid " + color}> <p style={"color: " + color}><b>{title}</b></p> {lines.map((line) => <p>{line}</p>).join("")} </div>; } }}} 使い方: #jsxcard(お知らせ, class=warn){{ 本文の1行目 本文の2行目 }} 出力: <div class="pkript-card pkript-warn" style="border-left:4px solid #c62"> <p style="color:#c62"><b>お知らせ</b></p> <p>本文の1行目</p><p>本文の2行目</p> </div> 本文に <b> のようなタグを書いても、{} の中の文字列は自動でエスケープされるので、タグとしては働きません。 詳しくは [[Pkript/文法/JSX]] を参照してください。 ** highlight - 文法ハイライト [#j76eed93] Pkriptのコードに対して色付けをします。 plugin/pkript/script/highlight.js #pkript(highlight){{{ #highlight{{{ // Pkript: Syntax Highlighter // Usage: // #pkript(highlight){{ // #highlight{{ // function hello(name) { // console.log("Hello, " + name); // } // }} function isAlpha(c) { return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c == "_" || c == "$"; } function isDigit(c) { return c >= "0" && c <= "9"; } function isAlphaNum(c) { return isAlpha(c) || isDigit(c); } const KEYWORDS = [ "function", "return", "let", "const", "var", "if", "else", "while", "for", "of", "in", "break", "continue", "switch", "case", "default", "new", "typeof", "instanceof", "try", "catch", "finally", "throw", "class", "extends", "import", "export", "from", "as", "async", "await", "yield", "this", "super", "delete", "void" ]; const BUILTINS = [ "true", "false", "null", "undefined", "NaN", "Infinity", "String", "Number", "Boolean", "Array", "Object", "Math", "JSON", "console", "document", "window", "Promise", "Symbol", "Date" ]; // Character sets for the scanner. spanWhile/spanUntil take a whole run in one // call; a character at a time costs 50-60 steps each. const IDENT_CHARS = "a-zA-Z0-9_$"; const NUM_CHARS = "0-9."; const SPACE_CHARS = " \t"; // Anything that can start a token of its own const TOKEN_START = "/\"'`0-9a-zA-Z_$"; function highlightJs(code) { let out = ""; let i = 0; const len = code.length; while (i < len) { const c = code.charAt(i); const c2 = i + 1 < len ? code.charAt(i + 1) : ""; // Single-line comment: // ... if (c == "/" && c2 == "/") { const start = i; i = code.spanUntil(i, "\n"); const comment = code.substring(start, i); out += "<span style=\"color: #308a27; font-style: italic;\">" + htmlsc(comment) + "</span>"; continue; } // Multi-line comment: /* ... */ if (c == "/" && c2 == "*") { const start = i; const end = code.indexOf("*/", i + 2); i = end < 0 ? len : end + 2; const comment = code.substring(start, i); out += "<span style=\"color: #308a27; font-style: italic;\">" + htmlsc(comment) + "</span>"; continue; } // String literals: "...", '...', `...` if (c == "\"" || c == "'" || c == "`") { const quote = c; // A backtick string may span lines; the other two may not const stop = quote == "`" ? "\\" + quote : "\\" + quote + "\n"; const start = i; i++; while (i < len) { i = code.spanUntil(i, stop); if (i >= len) { break; } const sc = code.charAt(i); if (sc == "\\") { i += 2; continue; } if (sc == quote) { i++; break; } break; // newline in a non-backtick string } const strVal = code.substring(start, i); out += "<span style=\"color: #032f62;\">" + htmlsc(strVal) + "</span>"; continue; } // Number literals if (isDigit(c)) { const start = i; i = code.spanWhile(i, NUM_CHARS); const numVal = code.substring(start, i); out += "<span style=\"color: #005cc5;\">" + htmlsc(numVal) + "</span>"; continue; } // Identifiers, Keywords, Builtins if (isAlpha(c)) { const start = i; i = code.spanWhile(i, IDENT_CHARS); const ident = code.substring(start, i); // Look ahead for function call: func(...) const lookAhead = code.spanWhile(i, SPACE_CHARS); const isCall = lookAhead < len && code.charAt(lookAhead) == "("; if (KEYWORDS.includes(ident)) { out += "<span style=\"color: #d73a49; font-weight: bold;\">" + htmlsc(ident) + "</span>"; } else if (BUILTINS.includes(ident)) { out += "<span style=\"color: #6f42c1;\">" + htmlsc(ident) + "</span>"; } else if (isCall) { out += "<span style=\"color: #005cc5;\">" + htmlsc(ident) + "</span>"; } else { out += htmlsc(ident); } continue; } // Punctuation and whitespace, taken as a run rather than one at a time const plain = code.spanUntil(i + 1, TOKEN_START); out += htmlsc(code.substring(i, plain)); i = plain; } return out; } function getCode(e) { if (e.body != "") return e.body; if (e.args.length > 0 && e.args[0] != "") return e.args[0]; return ""; } function plugin_highlight_convert(e) { const code = getCode(e); if (code == "") return "<p>(no code)</p>"; const highlighted = highlightJs(code); return "<pre style=\"background-color: #f6f8fa; color: #24292e; border: 1px solid #e1e4e8; font-family: monospace; font-size: 13px;\"><code>" + highlighted + "</code></pre>"; } }}} 使い方: #pkript(highlight){{ #highlight{{ function hello(name) { console.log("Hello, " + name); } }} ** recent - 更新時刻付きページ一覧 [#recent_sample] ページの最終更新時刻と凍結状態を一覧表示します。wiki.time, wiki.isFrozen, date.format, str.padStart を活用する例です。 plugin/pkript/script/recent.js #pkript(highlight){{ #highlight{{ function plugin_recent_convert(e) { const prefix = e.args[0] || ""; const pages = wiki.pages(prefix); if (pages.length == 0) return "<p>該当するページがありません。</p>"; // 各ページの更新時刻と凍結状態を取得 const items = pages.map((page) => { const t = wiki.time(page); return { page: page, time: t, frozen: wiki.isFrozen(page) }; }); // 更新時刻の新しい順にソート items.sort((a, b) => b.time - a.time); const rows = items.map((item, idx) => { const no = String(idx + 1).padStart(2, "0"); const frozenMark = item.frozen ? " <i>(凍結)</i>" : ""; const timeStr = date.format(item.time); return "<li>" + no + ". " + wiki.link(item.page) + " <small>[" + timeStr + "]</small>" + frozenMark + "</li>"; }).join("\n"); return "<ul>\n" + rows + "\n</ul>"; } }} 使い方: #recent(Docs/)