ខ្ញុំប្រើប្រាស់ Obsidian ដើម្បីសរសេរកំណត់ត្រា និងអត្ថបទផ្សេង។ ហើយនៅក្នុង Obsidian មានមុខងារដ៏ល្អក្នុងការបម្លែងតារាងធម្មតាទៅជាក្រាហ្វិក។ ខ្ញុំចង់បានភាពងាយស្រួលដូចគ្នានេះនៅលើវេបសាយនេះ ដូច្នេះខ្ញុំបានសាកល្បងកូពីអត្ថបទ markdown ពី Obsidian ចូលទៅក្នុងវេបសាយនេះតែម្តង តែគ្រប់ក្រាបទាំងអស់បានបង្ហាញចេញជាអត្ថបទធម្មតា!
មូលហេតុគឺជារឿងដែលគួរស្វែងយល់ ព្រោះវាជាអ្នកកំណត់ទម្រង់នៃដំណោះស្រាយទាំងមូល។ ទាំងនេះគឺជា Obsidian plugins។ ពួកវាសរសេរកំណត់ត្រារបស់អ្នកឡើងវិញនៅពេលអ្នកកំពុងមើលវានៅក្នុង Obsidian។ គេហទំព័រប្លុករបស់ខ្ញុំត្រូវបានបង្កើតឡើងដោយ Astro ដែល compile Markdown ជាមួយ pipeline ស្ដង់ដារ remark/rehype ដែលស្គាល់ CommonMark និង plugin ទាំងឡាយណាដែលអ្នកបានចុះឈ្មោះយ៉ាងច្បាស់លាស់។ Syntax របស់ Obsidian មិនមែនជាផ្នែកនៃរឿងទាំងនោះឡើយ។
ដូច្នេះគោលដៅបានក្លាយជា៖ សរសេរតារាង Markdown ធម្មតា បន្ថែមមួយជួរ រួចទទួលបានក្រាហ្វិកពិតប្រាកដ — ដោយមិនចាំបាច់ទាញយក framework មកប្រើឡើយ។
ដំបូង ខ្ញុំបានពិនិត្យមើលថាតើមានបណ្ណាល័យ (library) ណាដែលបានធ្វើរឿងនេះរួចហើយឬនៅ
មុនពេលសរសេរអ្វីមួយ ខ្ញុំបានស្វែងរកនៅលើ npm។ បេក្ខភាពដែលលេចធ្លោជាងគេបានបរាជ័យលើតម្រូវការទាំងពីរ៖
turn-to-chart— ឈ្មោះគឺត្រូវគ្នាយ៉ាងល្អឥតខ្ចោះ ប៉ុន្តែវាបង្កើតបានតែ ក្រាហ្វិកខ្សែ (line charts) ប៉ុណ្ណោះ។ គ្រប់ជម្រើសនៅក្នុង API របស់វាគឺទាក់ទងតែនឹងអ័ក្ស x និង y (xAxisType,yAxisFormat,yAxisOrigin) គឺគ្មានជម្រើសប្រភេទក្រាហ្វិក (chart-type) ទាល់តែសោះ ដូច្នេះគ្មានក្រាហ្វិកជួរឈរ (bar) ឬក្រាហ្វិកចំណិត (pie) ឡើយ។ វាក៏បានបញ្ជាក់reactនិងreact-domជា direct dependencies បូករួមទាំងកញ្ចប់@visx/*ចំនួនប្រាំមួយទៀត។ ហើយការចេញផ្សាយចុងក្រោយរបស់វាគឺនៅក្នុងខែវិច្ឆិកា ឆ្នាំ ២០២២។@tufte/remark-chart— ជា remark plugin ពិតប្រាកដ ដូច្នេះវានឹងដំណើរការជាមួយ Astro ហើយវាមានផ្តល់ជម្រើសbar,line,scatter,histogramនិងsparkline។ ប៉ុន្តែ គ្មានក្រាហ្វិកចំណិត (pie) ឡើយ ហើយ renderer របស់វាបង្កើតលទ្ធផលជា ASCII ដែលត្រូវបានរចនាឡើងសម្រាប់ terminal ច្រើនជាងទំព័រ web។- Vega-Lite — ល្អឥតខ្ចោះមែនទែន ប៉ុន្តែវាមិនផ្អែកលើតារាង (table-driven) នោះទេ។ អ្នកត្រូវសរសេរការកំណត់ (specification) ដោយដៃ ដែលវាមានភាពស៊ុករញ៉េរញ៉ៃ ផ្ទុយពីអ្វីដែលខ្ញុំព្យាយាមជៀសវាង។
សេចក្តីសន្និដ្ឋានដែលមានប្រយោជន៍៖ អ្វីដែលខ្ញុំចង់បានគឺមិនទាន់មានជាកញ្ចប់ទូទៅ (general-purpose package) ដែលមានការថែទាំនៅឡើយទេ។ ការងារដែលនៅសល់គឺមានត្រឹមតែប្រហែល ២៥ ជួរនៃកូដ ប៉ុណ្ណោះ ដែលតិចជាងដើមឈើ dependency (dependency tree) នៃជម្រើសទាំងនោះទៅទៀត។
របៀបដែលវាដំណើរការ
មានពីរផ្នែក ដោយមានការបែងចែកការងារយ៉ាងច្បាស់លាស់៖
Markdown source │ ├── <!--chart type="bar" height="280"--> ← a comment, invisible in Obsidian │ ├── | Quarter | Revenue | │ | ------- | ------- | │ | Q1 | 12,400 | ← a completely normal table │ ▼remark plugin → <div data-chart="bar" data-chart-height="280"></div> <div class="markdown-table-scroll"><table>…</table></div> (the table is left exactly as it was) ▼client script → reads the table, draws a canvas, then hides the tableតារាងទិន្នន័យមិនត្រូវបានកែប្រែឡើយ។ នេះគឺជាការសម្រេចចិត្តដ៏សំខាន់បំផុតក្នុងរចនាបទទាំងមូល ហើយវាគឺជាអ្វីដែលធ្វើឱ្យមុខងារនេះមានសុវត្ថិភាព៖
- ទិន្នន័យនៅតែជាអត្ថបទពិតប្រាកដនៅក្នុងទំព័រ ដូច្នេះ Pagefind អាចស្វែងរកវាបាន។
- កម្មវិធីអានអេក្រង់ (Screen readers) អាចអានវាបាន។
- ប្រសិនបើ JavaScript បរាជ័យ CDN មិនអាចតភ្ជាប់បាន ឬលេខមិនអាច parse បាន អ្នកអាននៅតែមើលឃើញតារាងដដែល។
តារាងត្រូវបានលាក់ លុះត្រាតែក្រាហ្វិកត្រូវបានគូររួចរាល់ពិតប្រាកដ។
ជំហានទី ១៖ remark plugin
ការងារទាំងមូលរបស់ plugin គឺបម្លែង comment ទៅជាធាតុចំណាំ (marker element) ដែលស្ថិតនៅចំពីមុខតារាង។
src/plugins/remark-table-chart.ts:
type MdastNode = { type?: string; value?: string; data?: { hName?: string; hProperties?: Record<string, unknown>; }; children?: MdastNode[];};
// <!--chart type="bar" height="300" title="Revenue"-->const DIRECTIVE_PATTERN = /^<!--\s*chart\b([^>]*?)\s*-->$/;
const CHART_TYPES = new Set(['bar', 'line', 'pie', 'stacked-bar']);
const MAX_HEIGHT = 720;const MIN_HEIGHT = 120;
function parseAttributes(source: string) { const attributes: Record<string, string> = {};
for (const match of source.matchAll(/([a-zA-Z][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) { attributes[match[1].toLowerCase()] = match[2] ?? match[3] ?? ''; }
return attributes;}
function createMarker(attributes: Record<string, string>): MdastNode { const type = (attributes.type ?? 'bar').toLowerCase(); const properties: Record<string, unknown> = { dataChart: CHART_TYPES.has(type) ? type : 'bar', };
const height = Number.parseInt(attributes.height ?? '', 10);
if (Number.isFinite(height)) { properties.dataChartHeight = String(Math.min(Math.max(height, MIN_HEIGHT), MAX_HEIGHT)); }
if (attributes.title) { properties.dataChartTitle = attributes.title; }
if (attributes.unit) { properties.dataChartUnit = attributes.unit; }
return { type: 'paragraph', data: { hName: 'div', hProperties: properties }, children: [], };}
export default function remarkTableChart() { return (tree: MdastNode) => { const children = tree.children;
if (!children) return;
for (let index = 0; index < children.length; index += 1) { const node = children[index];
if (node.type !== 'html' || typeof node.value !== 'string') continue;
const match = node.value.trim().match(DIRECTIVE_PATTERN);
if (!match) continue;
// Only convert when a table actually follows, so an unrelated comment // that happens to start with "chart" is left alone. if (children[index + 1]?.type !== 'table') continue;
children[index] = createMarker(parseAttributes(match[1] ?? '')); } };}មានលម្អិតពីរចំណុចនៅក្នុងនោះដែលជាចំណុចគន្លឹះ៖
-
children[index + 1]?.type !== 'table'— comment ត្រូវបានបម្លែងតែនៅពេលដែលមានតារាងស្ថិតនៅបន្ទាប់បន្សំពិតប្រាកដ។ បើគ្មានចំណុចនេះទេ comment ផ្សេងទៀតដែលចាប់ផ្តើមដោយពាក្យ “chart” នឹងក្លាយជា marker ដែលខូចដោយស្ងាត់ៗ។ -
CHART_TYPES.has(type) ? type : 'bar'— ប្រភេទដែលមិនស្គាល់នឹងធ្លាក់ទៅប្រើ bar ជំនួសឱ្យការ throw error ដូច្នេះប្រសិនបើសរសេរខុសអក្ខរាវិរុទ្ធ វានឹងថយចុះកម្រិត (degrade) ជាជាងធ្វើឱ្យ build ខូច។ ការចុះឈ្មោះនៅក្នុងastro.config.mjs៖
markdown: { // Khmer is unspaced, so the built HTML gets invisible word breaks first. remarkPlugins: [remarkKhmerBreaks, remarkTableChart, ...astroPluginConfig.remarkPlugins], rehypePlugins: astroPluginConfig.rehypePlugins,},The output is a single empty element that carries the options:
<div data-chart="bar" data-chart-height="280" data-chart-title="Revenue by quarter"></div>ជំហានទី ២៖ ស្គ្រីប client
ការ Parse តារាង
ជួរឈរដំបូងគេក្លាយជាស្លាកប្រភេទ (category labels); ជួរឈរបន្ទាប់បន្សំទាំងអស់ក្លាយជា series ដូច្នេះក្រាហ្វិកដែលមាន multi-series មិនចាំបាច់មាន syntax បន្ថែមឡើយ។
// Khmer digits (០-៩) are not valid to Number(), so a table written with them// would otherwise parse as a column of NaN and never render.const KHMER_DIGITS = ['០', '១', '២', '៣', '៤', '៥', '៦', '៧', '៨', '៩'];
function toNumber(value: string): number | null { const normalized = value.replace(/[០-៩]/g, (digit) => String(KHMER_DIGITS.indexOf(digit))); const cleaned = normalized.replace(/[\s,]/g, '').replace(/%$/, '');
if (cleaned === '' || cleaned === '-' || cleaned === '—') return null;
const parsed = Number(cleaned);
return Number.isFinite(parsed) ? parsed : null;}
function parseTable(table: HTMLTableElement): ChartTable | null { const headers = Array.from(table.querySelectorAll('thead th')).map((cell) => (cell.textContent ?? '').trim(), );
if (headers.length < 2) return null;
const rows = Array.from(table.querySelectorAll('tbody tr')); const labels: string[] = []; const columns: number[][] = Array.from({ length: headers.length - 1 }, () => []);
rows.forEach((row) => { const cells = Array.from(row.querySelectorAll('td'));
if (cells.length < 2) return;
labels.push((cells[0].textContent ?? '').trim());
for (let column = 1; column < headers.length; column += 1) { columns[column - 1].push(toNumber(cells[column]?.textContent ?? '') ?? Number.NaN); } });
if (labels.length === 0) return null;
// A column is only chartable when every value in it parsed as a number. const series = columns .map((values, index) => ({ name: headers[index + 1], data: values })) .filter((entry) => !entry.data.some((value) => Number.isNaN(value)));
if (series.length === 0) return null;
return { labels, series };}filter គឺជាបណ្តាញការពារសុវត្ថិភាព។ ប្រសិនបើប្រអប់ (cell) តែមួយនៅក្នុងជួរឈរមិនមែនជាលេខទេ Series ទាំងមូលនោះនឹងត្រូវបោះបង់ចោល ជំនួសឱ្យការយកទៅប្លូតជាលេខសូន្យដែលនាំឱ្យយល់ច្រឡំ។ ប្រសិនបើ គ្រប់ series ទាំងអស់ត្រូវបានបោះបង់ចោល parseTable នឹងត្រឡប់មកវិញនូវ null ហើយអ្នកហៅប្រើប្រាស់នឹងទុកឱ្យតារាងនៅតែមើលឃើញដដែល។
ការដើរតាម theme របស់គេហទំព័រ
ពណ៌អត្ថបទ, grid និង tooltip ត្រូវបានអានចេញពី CSS variables របស់គេហទំព័រផ្ទាល់ ដូច្នេះក្រាហ្វិកនឹងត្រូវគ្នាទៅតាម palette និង theme ដែលកំពុងសកម្ម។ ពណ៌របស់ Series ត្រូវកំណត់ជាក់លាក់ (hardcoded) ព្រោះពណ៌ accent តែមួយមិនអាចបង្កើត series ប្រាំមួយដែលមានលក្ខណៈខុសគ្នាស្រឡះដោយភ្នែកបានឡើយ៖
function themeColors() { const styles = getComputedStyle(document.documentElement); const read = (name: string, fallback: string) => { const value = styles.getPropertyValue(name).trim();
return value || fallback; };
const isDark = document.documentElement.dataset.theme === 'dark';
return { text: read('--paper-ink', isDark ? '#e7e7ea' : '#1c1e21'), muted: read('--paper-ink-soft', isDark ? '#a6a6ad' : '#5c6066'), grid: read('--paper-line', isDark ? '#33353a' : '#e2e2e6'), surface: read('--paper-bg', isDark ? '#131417' : '#ffffff'), };}
const SERIES_COLORS = ['#4c8d6d', '#5b7fd4', '#c2703d', '#8d6bb5', '#3f9aa5', '#b8556f'];សម្រាប់ការប្តូរ theme គេហទំព័រកំណត់ data-theme លើ <html> ដូច្នេះ MutationObserver គឺគ្រប់គ្រាន់ហើយ៖
const observer = new MutationObserver(() => { void paint(markers().filter((marker) => marker.dataset.chartPainted === 'true'));});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'],});ការគូរឡើងវិញប្រើប្រាស់ canvas ដែលមានស្រាប់ឡើងវិញ (existing?.destroy()) ជំនួសឱ្យការបង្កើតថ្មី ដូច្នេះការប្តូរ theme មិនធ្វើឱ្យទំព័រលោតផ្លោះឡើយ។
Bugs ចំនួនបីដែលខ្ញុំបានជួប និងមូលហេតុរបស់វា
នេះគឺជាផ្នែកដែលចំណាយពេលរបស់ខ្ញុំពិតប្រាកដ ដូច្នេះវាជារឿងដែលគួរតែកត់ត្រាទុក។
១. ក្រាហ្វិករីកធំឡើងដោយគ្មានដែនកំណត់
នេះមើលទៅដូចជា rendering bug ហើយវាអាក្រក់បំផុតក្នុងចំណោមទាំងបី ព្រោះវាពិបាករកហេតុផលបំផុត។ ខ្ញុំបានកំណត់កម្ពស់នៅលើ canvas៖
canvas.style.height = `${height}px`; // this is the bugChart.js ដែលមាន responsive: true វាស់ container របស់វាដើម្បីសម្រេចទំហំ canvas។ ដោយសារតែ canvas គឺជាអ្វីដែលកំពុងត្រូវវាស់ និងជាអ្វីដែលកំពុងកំណត់កម្ពស់ក្នុងពេលតែមួយ នោះការរត់នីមួយៗបង្កើតបានលទ្ធផលធំជាងមុនបន្តិចបន្តួច៖
Chart.js កំណត់ទំហំ canvas ពី container របស់វា → container រីកធំដោយសារតែ canvas រីកធំ → Chart.js កំណត់ទំហំ canvas ពី container ម្តងទៀត…
ដំណោះស្រាយគឺត្រូវដាក់កម្ពស់នៅលើ wrapper ហើយមិនត្រូវដាក់នៅលើ canvas ឡើយ៖
plot = document.createElement('div');plot.className = 'table-chart__plot';plot.style.height = `${plotHeight}px`;
const canvas = document.createElement('canvas');plot.append(canvas);CSS ក៏សំខាន់ដូចគ្នាដែរ។ ខ្ញុំក៏ត្រូវលុប width: 100% !important ចេញពី canvas ផងដែរ ព្រោះការបង្ខំវិមាត្រណាមួយនឹងជះឥទ្ធិពលប្រឆាំងនឹងការកំណត់ទំហំផ្ទាល់របស់ Chart.js៖
.table-chart__plot { position: relative; width: 100%;}
.table-chart__plot canvas { display: block;}ខ្ញុំមិនទុកចិត្តតែលទ្ធផលមើលឃើញដោយភ្នែកទទេ។ ដូច្នេះខ្ញុំបានប្រើប្រាស់ browser ពិតប្រាកដ និងធ្វើការតេស្តទំហំចំនួន ៦ ដងក្នុងរយៈពេល ៤ វិនាទី៖
t=0: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260t=1: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260t=2: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260t=3: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260t=4: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260t=5: 280px plot=280 canvas=1104x280 | 260px plot=260 canvas=1104x260លទ្ធផល: STABLE២. តារាងមិនស្ថិតនៅកន្លែងដែលខ្ញុំរំពឹងទុក
គ្មានក្រាហ្វិកណាមួយបង្ហាញទាល់តែសោះ។ មូលហេតុ៖ plugin responsiveTables ដែលមកជាមួយ theme នេះ ខ្ចប់គ្រប់តារាងទាំងអស់នៅក្នុង <div class="markdown-table-scroll"> ដូច្នេះ nextElementSibling គឺជា div ហើយមិនដែលជា HTMLTableElement ឡើយ។
// Would never matchconst table = marker.nextElementSibling;
// The table is the next sibling, or the first table inside itconst sibling = marker.nextElementSibling;const table = sibling instanceof HTMLTableElement ? sibling : sibling?.querySelector('table');វាជារឿងល្អដែលត្រូវពិនិត្យមើល output នៃ theme របស់ប្រព័ន្ធអ្នក មុនពេលសន្មតថាទំនាក់ទំនង sibling ត្រូវបានរក្សា។
៣. លេខខ្មែរមិនអាចអានបាន ហើយគ្មានចេញError
ចំណុចនេះលេចឡើងតែដោយសារតែខ្ញុំបានសរសេរអត្ថបទជាភាសាខ្មែរ និងបានប្រើប្រាស់លេខខ្មែរពិតប្រាកដ។
Number('១២០') // NaNក្រាហ្វិកមិនបង្ហាញ error មិនផ្តល់ការព្រមាន ហើយគ្រាន់តែមិនបង្ហាញចេញមកតែម្តង។ ឥឡូវនេះ toNumber ធ្វើឱ្យលេខខ្មែរក្លាយជា ASCII ជាមុនសិន ដែលជាជួរនៅផ្នែកខាងលើនៃ parser ខាងលើ។ ប្រសិនបើគេហទំព័ររបស់អ្នកមិនមែនជាភាសាខ្មែរទេ អ្នកអាចលុបជួរនោះចេញបាន — ប៉ុន្តែវាគឺជាឧទាហរណ៍ដ៏ល្អនៃបរាជ័យដែលកើតឡើងដោយស្ងៀមស្ងាត់ទាំងស្រុង។
ការប្រៀបធៀប និងរបៀបប្រើប្រាស់
បន្ថែម comment មួយជួរនៅពីមុខតារាងណាមួយ៖
<!--chart type="bar" title="Revenue by quarter" height="280"-->| Quarter | Revenue | Costs || ------- | ------- | ----- || Q1 | 12,400 | 8,100 || Q2 | 15,900 | 9,300 || Attribute | Values | Default | Purpose |
|---|---|---|---|
type | bar, line, stacked-bar, pie | bar | ប្រភេទក្រាប |
title | អក្សរអ្វីក៏បាន | — | ចំណងជើងនៅលើក្រាប |
height | 120–720 | 300 | កម្ពស់របស់ Plot ជា pixels |
unit | អក្សរអ្វីក៏បាន | — | អក្សរសម្រាប់ tooltip |
លេខអាចត្រូវបានសរសេរដោយមានសញ្ញាខណ្ឌពាន់ សញ្ញា % នៅខាងចុង ឬជាលេខខ្មែរ។
ឧទាហរណ៍ផ្ទាល់ ដែលត្រូវបាន render នៅលើទំព័រនេះ៖
| Day | Signups |
|---|---|
| Mon | 120 |
| Tue | 180 |
| Wed | 150 |
| Thu | 240 |
| Fri | 310 |
ហើយនេះគឺជាករណីបរាជ័យដោយចេតនា។ គ្រប់តម្លៃខាងក្រោមគឺជាអត្ថបទ ដូច្នេះគ្មាន series ណាអាចបង្កើតបានឡើយ ដូច្នេះតារាងនៅតែស្ថិតនៅកន្លែងដើមដដែល៖
| Name | Note |
|---|---|
| Ada | lots |
| Grace | some |
ផលប៉ះពាល់
មិនមានអ្វីត្រូវដំឡើងឡើយ។ គ្មាន React, គ្មាន framework, គ្មានជំហាន build។ Chart.js ត្រូវបានទាញយកពី CDN តែនៅពេលដែលទំព័រមានក្រាហ្វិកពិតប្រាកដប៉ុណ្ណោះ ហើយវាត្រូវបានរក្សាទុក (cached) ដោយ browser បន្ទាប់ពីនោះ — ទំព័រដែលគ្មានក្រាហ្វិក នឹងមិនទាញយកវាឡើយ។
ការអនុវត្តទាំងមូលគឺមានត្រឹមតែ remark plugin មួយ និងស្គ្រីបមួយប៉ុណ្ណោះ។ សម្រាប់ការប្រៀបធៀប ការបន្ថែម Recharts ទៅកាន់គេហទំព័រនេះ មានន័យថាត្រូវដំឡើង @astrojs/react, react, react-dom និង recharts ដែលសរុបមកមានទំហំប្រហែល ៥ ដងនៃទំហំ JavaScript សរុបបច្ចុប្បន្នរបស់គេហទំព័រ នៅលើប្លុកដែលបច្ចុប្បន្នផ្ញើចេញប្រហែល 39 KB នៃ JS ដែលគ្មាន framework ក្នុងមួយអត្ថបទទេ។
ជាសង្ខេប
- តារាង Markdown បូករួមទាំង comment មួយជួរ ក្លាយជាក្រាហ្វិក។ គ្មាន syntax បន្ថែមសម្រាប់ទិន្នន័យ multi-series ឡើយ។
- តារាងគឺជាប្រភពនៃការពិត (source of truth) ហើយមិនដែលត្រូវបានលុបចោលឡើយ លុះត្រាតែក្រាហ្វិកត្រូវបាន render យ៉ាងពិតប្រាកដ។
- គ្មាន dependencies ថ្មី គ្មាន framework គ្មានការកំណត់រចនាសម្ព័ន្ធ build។
- ចំណាយត្រឹមតែកូដ parse ប្រហែល ២៥ ជួរ បូករួមទាំងពេលវេលា debugging សម្រាប់ bug ទាំងបីខាងលើ។
ប្រសិនបើអ្នកកំពុងបង្កើតគេហទំព័រដែលក្រាហ្វិកគ្រាន់តែជាផ្នែករង នេះគឺជាជម្រើសដ៏ល្អ។ ប៉ុន្តែប្រសិនបើក្រាហ្វិកគឺជា គោលបំណងចម្បង នៃគេហទំព័រ component សមស្របមួយដែលមាន Vega-Lite ឬ Recharts នឹងមានសមត្ថភាពពង្រីក (scale) បានល្អជាង — ប៉ុន្តែនោះគឺជាគម្រោងផ្សេងពីនេះ។