-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutil.js
More file actions
119 lines (110 loc) · 9.81 KB
/
Copy pathutil.js
File metadata and controls
119 lines (110 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// ตัวช่วยที่ทุกแท็บใช้ร่วมกัน -- ฝั่ง renderer ล้วน ไม่ใช้ Node API
//
// ต้องโหลดเป็นไฟล์แรกสุด ก่อน uidump.js / meetingrun.js / tab-*.js ทั้งหมด
//
// ห่อด้วย IIFE ไม่ใช่ ES module เพราะ widget.html ถูกโหลดผ่าน file:// ซึ่ง
// Chromium บล็อก <script type="module"> ด้วย CORS
//
// เหตุผลที่ต้องมีไฟล์นี้: ตอน v1.8.2 esc() งอกขึ้นมาสามตัวคนละแบบ (widget.html, uidump.js,
// meetingrun.js) และไม่มีตัวไหน escape " ทั้งที่ถูกใช้ใน attribute สิบสี่จุด -- ปล่อยให้แต่ละไฟล์
// พก helper ส่วนตัวแล้วมันงอกจริง
//
// ตอนนี้ฝั่งแอปเหลือตัวนี้ตัวเดียวแล้ว แต่ยังมี esc อีกตัวใน uidump.js ที่ "ห้ามยุบเข้ามา":
// มันอยู่ในเทมเพลตของ tpl() คือโค้ดที่ถูกฝังไปรันในวิวเวอร์ failure.xml ซึ่งเป็น iframe
// sandbox="allow-scripts" (ไม่มี allow-same-origin) -- คนละ document แตะ window.COWORK
// ของแอปไม่ได้เลย เป็นคนละโปรแกรมกัน ไม่ใช่ก๊อปซ้ำ
(function (global) {
'use strict';
const D = ['อาทิตย์', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', 'เสาร์'];
const M = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];
// textContent→innerHTML แปลง & < > ให้ แต่ไม่แตะ " -- และ esc() ถูกใช้ใน attribute หลายที่
// (title=, data-g=, value=) พิมพ์ " ในช่องค้นหาบทถอดเสียงแล้ว value ขาดกลางคัน
// แถมงอก attribute ขยะขึ้นมาจริง ๆ จึงต้อง escape " ต่อท้ายอีกชั้น
// ไม่ต้องแตะ ' เพราะทุก attribute ในแอปนี้ครอบด้วย " ทั้งหมด
//
// ห้ามเขียนใหม่เป็น regex ทั้งก้อน: ทางนี้แปลง U+00A0 เป็น ด้วย regex ไม่แปลง
// ผลลัพธ์จะต่างโดยที่ไม่มีอะไรฟ้อง
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML.replace(/"/g, '"');
}
function hashN(key, n) {
let h = 0;
for (const c of String(key)) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return h % n;
}
function timeAgo(iso) {
if (!iso) return 'ไม่ระบุ';
const diff = Date.now() - new Date(iso).getTime();
const min = Math.floor(diff / 60000);
if (min < 1) return 'เมื่อสักครู่';
if (min < 60) return `${min} นาทีที่แล้ว`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr} ชม.ที่แล้ว`;
const day = Math.floor(hr / 24);
if (day < 30) return `${day} วันที่แล้ว`;
const mon = Math.floor(day / 30);
if (mon < 12) return `${mon} เดือนที่แล้ว`;
return `${Math.floor(mon / 12)} ปีที่แล้ว`;
}
// รอบรีเฟรชอัตโนมัติ (นาที) — 0 = ปิด · ตัวเลือกสำเร็จรูป ไม่ใช่ช่องพิมพ์ตัวเลข เพราะพิมพ์
// 0.1 ได้แปลว่ายิง Redmine API ทุก 6 วินาทีทั้งวัน ซึ่งไม่มีใครตั้งใจและมองไม่เห็นจากในแอป
//
// ไฟล์นี้เป็นฝั่ง renderer ก็จริง แต่สองค่านี้ main.js ก็ require ไปใช้ (ตอนโหลด/บันทึก config)
// เพราะลิสต์ตัวเลือกกับด่านกันค่าเพี้ยนต้องเป็นตัวเดียวกันทั้งสองฝั่ง แยกเมื่อไหร่ก็มีวันที่
// เมนูให้เลือกค่าที่ main ปฏิเสธ — ตัวอื่นใน util.js แตะ document, สองตัวนี้ไม่แตะ จึง require ได้
const REFRESH_CHOICES = [0, 1, 2, 5, 10, 15, 30];
const DEFAULT_REFRESH_MINUTES = 5;
// config.json ผู้ใช้เปิดแก้เองได้ และ <select>.value เป็นสตริงเสมอ — ทั้งสองทางเป็น input
// ภายนอกที่ไว้ใจไม่ได้ ค่าที่ไม่ตรงตัวเลือกไหนเลยตกกลับค่าเริ่มต้น ไม่ใช่ปิดการรีเฟรชทิ้ง
//
// ต้องกรองชนิดก่อนเรียก Number() ไม่ใช่พึ่ง Number() อย่างเดียว: Number(null), Number(''),
// Number([]) ได้ 0 ทั้งหมด ซึ่งบังเอิญเป็นตัวเลือกที่ถูกต้อง (= ปิด) — คีย์ที่หายไปจาก config
// จึงจะกลายเป็น "ผู้ใช้สั่งปิดรีเฟรช" เงียบ ๆ แทนที่จะเป็นค่าเริ่มต้น 5 นาที
function normalizeRefreshMinutes(v) {
if (typeof v === 'string' && v.trim() === '') return DEFAULT_REFRESH_MINUTES;
if (typeof v !== 'number' && typeof v !== 'string') return DEFAULT_REFRESH_MINUTES;
const n = Number(v);
return REFRESH_CHOICES.includes(n) ? n : DEFAULT_REFRESH_MINUTES;
}
// เพดานเวลาต่อไฟล์เทส (นาที) — 0 = ไม่จับเวลา · เหตุผลเดียวกับ REFRESH_CHOICES ทุกข้อ
// (ตัวเลือกสำเร็จรูป ไม่ใช่ช่องพิมพ์ตัวเลข · main.js require ไปใช้ตัวเดียวกัน)
//
// ค่าเริ่มต้น 20 นาที (ผู้ใช้เลือก 2026-08-10): เทสที่ค้างต้องถูกฆ่าเพื่อให้ชุดเดินต่อได้ตอน
// ไม่มีคนเฝ้า แต่ตั้งสั้นเกินไปจะไปฆ่าเทสที่กำลังทำงานปกติ แล้วทิ้งออเดอร์ค้างบน dev ให้ตามเก็บ
const TESTRUN_TIMEOUT_CHOICES = [0, 5, 10, 20, 30, 60];
const DEFAULT_TESTRUN_TIMEOUT = 20;
function normalizeTestRunTimeout(v) {
if (typeof v === 'string' && v.trim() === '') return DEFAULT_TESTRUN_TIMEOUT;
if (typeof v !== 'number' && typeof v !== 'string') return DEFAULT_TESTRUN_TIMEOUT;
const n = Number(v);
return TESTRUN_TIMEOUT_CHOICES.includes(n) ? n : DEFAULT_TESTRUN_TIMEOUT;
}
// สลับ path เก่า (default ยุคก่อน testingroom, 2026-08-09) เป็นบ้านใหม่ ตอนโหลด config —
// เทียบแบบตัดสแลชท้าย + ไม่สนตัวพิมพ์ (Windows path) · path ที่ user ตั้งเองไม่ match ก็ไม่แตะ
function migrateSourcePaths(sources, map) {
// config.json แก้เองด้วยมือได้ ทั้งชนิดสแลช (/ กับ \) และช่องว่างหัว/ท้ายที่หลุดติดมา
// ตอนแก้ไฟล์ จึงต้อง trim + รวมสแลชเป็นแบบเดียว (\\) ก่อนตัดสแลชท้ายและตัดตัวพิมพ์
const norm = p => String(p || '').trim().replace(/\//g, '\\').replace(/[\\]+$/, '').toLowerCase();
const table = {};
for (const [k, v] of Object.entries(map || {})) table[norm(k)] = v;
let changed = false;
const out = (Array.isArray(sources) ? sources : []).map(s => {
const hit = s && s.path && table[norm(s.path)];
if (!hit) return s;
changed = true;
return { ...s, label: hit.label, path: hit.path };
});
return { sources: out, changed };
}
global.COWORK = global.COWORK || {};
global.COWORK.tabs = global.COWORK.tabs || {};
global.COWORK.util = { D, M, esc, hashN, timeAgo, REFRESH_CHOICES, normalizeRefreshMinutes,
TESTRUN_TIMEOUT_CHOICES, normalizeTestRunTimeout, migrateSourcePaths };
// ให้ node --test เรียกใช้ได้โดยไม่ต้องมี DOM (esc ต้องมี document จึงเทสแยกไม่ได้ ตัวอื่นเทสได้)
if (typeof module !== 'undefined' && module.exports) {
module.exports = { D, M, hashN, timeAgo, REFRESH_CHOICES, DEFAULT_REFRESH_MINUTES, normalizeRefreshMinutes,
TESTRUN_TIMEOUT_CHOICES, DEFAULT_TESTRUN_TIMEOUT, normalizeTestRunTimeout, migrateSourcePaths };
}
})(typeof window !== 'undefined' ? window : globalThis);