/* ============================================================
Enarge prototype — data layer
- block-based article model (text / code / image)
- multi-language syntax highlighter (C# / C / C++ / Java / Kotlin / Swift)
- image resize helper (downscale + JPEG to fit localStorage)
Exposes: window.EnargeData, window.highlightCode, window.resizeImage
============================================================ */
/* ---------- Syntax highlighter ---------- */
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(//g, ">");
}
/* union of common keywords across the C-family languages we support */
const KEYWORDS = new Set((
// shared / C#
"abstract as async await base bool break byte case catch char checked class const continue " +
"decimal default delegate do double else enum event explicit extern false finally fixed float " +
"for foreach get goto if implicit in int interface internal is lock long namespace new null " +
"object operator out override params private protected public readonly ref return sbyte sealed " +
"set short sizeof stackalloc static string struct switch this throw true try typeof uint ulong " +
"unchecked unsafe ushort using var virtual void volatile while record nameof yield when value " +
// C / C++
"auto constexpr nullptr template typename friend inline mutable namespace explicit operator " +
"register signed unsigned union typedef struct enum sizeof include define ifdef ifndef endif " +
"pragma std vector throw noexcept decltype final override " +
// Java / Kotlin
"import package extends implements instanceof synchronized transient native strictfp throws " +
"fun val var suspend companion data sealed open lateinit by reified inline crossinline " +
"init constructor object annotation enum interface " +
// Swift
"func let guard defer protocol extension typealias associatedtype where inout mutating " +
"nonmutating weak unowned lazy rethrows some any willSet didSet repeat fallthrough indirect convenience required"
).split(/\s+/));
function highlightCode(raw) {
const src = String(raw).replace(/\r\n/g, "\n");
let out = "";
let i = 0;
const n = src.length;
const isIdentStart = c => /[A-Za-z_@#]/.test(c);
const isIdent = c => /[A-Za-z0-9_]/.test(c);
while (i < n) {
const c = src[i];
const two = src.substr(i, 2);
if (two === "//") { let j = src.indexOf("\n", i); if (j === -1) j = n; out += `${escapeHtml(src.slice(i, j))}`; i = j; continue; }
if (two === "/*") { let j = src.indexOf("*/", i + 2); j = j === -1 ? n : j + 2; out += `${escapeHtml(src.slice(i, j))}`; i = j; continue; }
if (c === '"' || c === "'" || c === "`") {
let j = i + 1; while (j < n && src[j] !== c) { if (src[j] === "\\") j++; j++; }
j = Math.min(j + 1, n); out += `${escapeHtml(src.slice(i, j))}`; i = j; continue;
}
if (/[0-9]/.test(c)) { let j = i; while (j < n && /[0-9a-fA-FxXuUlLfF._]/.test(src[j])) j++; out += `${escapeHtml(src.slice(i, j))}`; i = j; continue; }
if (isIdentStart(c)) {
let j = i; if (c === "@" || c === "#") j++; while (j < n && isIdent(src[j])) j++;
const word = src.slice(i, j);
let k = j; while (k < n && src[k] === " ") k++;
const bare = word.replace(/^[@#]/, "");
if (word[0] === "#") out += `${escapeHtml(word)}`;
else if (word[0] === "@") out += `${escapeHtml(word)}`;
else if (KEYWORDS.has(bare)) out += `${escapeHtml(word)}`;
else if (/^[A-Z]/.test(word)) out += `${escapeHtml(word)}`;
else if (src[k] === "(") out += `${escapeHtml(word)}`;
else out += escapeHtml(word);
i = j; continue;
}
out += escapeHtml(c); i++;
}
return out;
}
window.highlightCode = highlightCode;
/* ---------- Image resize (downscale + JPEG) ---------- */
window.resizeImage = function (file, maxDim = 1280, quality = 0.82) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
const img = new Image();
img.onerror = reject;
img.onload = () => {
let { width, height } = img;
if (width > maxDim || height > maxDim) {
const r = Math.min(maxDim / width, maxDim / height);
width = Math.round(width * r); height = Math.round(height * r);
}
const canvas = document.createElement("canvas");
canvas.width = width; canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL("image/jpeg", quality));
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
};
/* ---------- Seed articles (block model) ---------- */
const T = (text) => ({ type: "text", text });
const C = (lang, text) => ({ type: "code", lang, text });
const SEED = [
{
id: "a-001",
category: "C#情報",
title: "async/await の例外を取りこぼさないための ContinueWith パターン",
excerpt: "非同期処理で例外が握り潰される典型ケースと、Task の継続処理で確実にログへ流す書き方。",
author: "自分", date: "2026-05-28", readMin: 6, favorite: true,
tags: ["async", "例外処理", ".NET8"],
blocks: [
T("UI スレッドを止めずに重い処理を回すとき、`async void` を多用すると例外が表に出てきません。"),
T("下記のように継続処理側で必ず観測することで、握り潰しを防げます。"),
C("csharp", `public async Task RunSafelyAsync(CancellationToken ct)
{
var task = Task.Run(() => Measure(ct), ct);
await task.ContinueWith(t =>
{
if (t.IsFaulted)
_logger.Error(t.Exception, "計測処理で例外");
}, TaskScheduler.Default);
}`)
]
},
{
id: "a-002",
category: "マイコン情報",
title: "STM32 の DMA + リングバッファで UART を取りこぼさない受信設計",
excerpt: "バイト割り込み方式をやめて DMA 循環モードに切り替えた際の設計メモ。",
author: "自分", date: "2026-05-19", readMin: 8, favorite: false,
tags: ["STM32", "DMA", "UART"],
blocks: [
T("高ボーレートで連続受信すると、バイト割り込み方式では取りこぼしが発生します。"),
T("DMA を循環モードにして、IDLE 割り込みで受信長を確定する構成が安定します。"),
C("cpp", `void USART1_IRQHandler(void)
{
if (LL_USART_IsActiveFlag_IDLE(USART1)) {
LL_USART_ClearFlag_IDLE(USART1);
uint16_t pos = RX_SIZE - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_5);
ring_push(&rx_ring, dma_buf, pos);
}
}`)
]
},
{
id: "a-003",
category: "SEIMI規格情報",
title: "SEIMI 規格における計測データのタイムスタンプ整合性チェック",
excerpt: "規格準拠でデータを記録する際の、時刻同期ズレを検出するバリデーション手順。",
author: "自分", date: "2026-05-08", readMin: 5, favorite: false,
tags: ["SEIMI", "規格", "バリデーション"],
blocks: [
T("複数台の計測器をまたいで記録する場合、各機器の時刻同期ズレが規格許容値を超えていないか確認が必要です。"),
C("csharp", `var drifted = records
.Where(r => Math.Abs((r.DeviceTime - r.MasterTime).TotalMilliseconds) > 50)
.OrderByDescending(r => r.DeviceTime)
.ToList();
if (drifted.Any())
throw new SeimiValidationException($"時刻ズレ {drifted.Count} 件");`)
]
},
{
id: "a-004",
category: "Android情報",
title: "Kotlin Coroutine + Flow でセンサー値をUIに流す最小構成",
excerpt: "計測機器のセンサー値を callbackFlow で受けて、UI に安全に反映するメモ。",
author: "自分", date: "2026-04-30", readMin: 4, favorite: false,
tags: ["Kotlin", "Coroutine", "Flow"],
blocks: [
T("コールバック型のセンサーAPIを `callbackFlow` で包むと、ライフサイクルに合わせて購読を止められます。"),
C("kotlin", `fun sensorFlow(sensor: Sensor): Flow = callbackFlow {
val listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) { trySend(e.values[0]) }
override fun onAccuracyChanged(s: Sensor?, a: Int) {}
}
manager.registerListener(listener, sensor, SENSOR_DELAY_UI)
awaitClose { manager.unregisterListener(listener) }
}`)
]
}
];
/* ---------- Store (localStorage backed) ---------- */
const STORE_KEY = "enarge_articles_v2";
const EnargeData = {
categories: ["C#情報", "C++情報", "マイコン情報", "SEIMI規格情報", "Android情報", "iOS情報", "回路・ハードウェア"],
langs: [
{ id: "csharp", label: "C#" },
{ id: "cpp", label: "C / C++" },
{ id: "kotlin", label: "Java / Kotlin" },
{ id: "swift", label: "Swift" },
{ id: "text", label: "テキスト" }
],
password: "enarge33!",
load() {
try { const raw = localStorage.getItem(STORE_KEY); if (raw) return JSON.parse(raw); } catch (e) {}
return SEED.map(a => Object.assign({}, a));
},
save(list) { try { localStorage.setItem(STORE_KEY, JSON.stringify(list)); return true; } catch (e) { return false; } },
reset() { try { localStorage.removeItem(STORE_KEY); } catch (e) {} return SEED.map(a => Object.assign({}, a)); },
langLabel(id) { const l = this.langs.find(x => x.id === id); return l ? l.label : id; }
};
window.EnargeData = EnargeData;