From 20aa726d3f9dae50000b5d3bef7a0add1a914c5d Mon Sep 17 00:00:00 2001 From: Xubai Wang <18016038327@189.cn> Date: Tue, 30 Sep 2025 15:44:54 +0800 Subject: [PATCH 1/3] feat: implement Jupyter text/html output --- src/table.ts | 43 ++++++++++++++++++++++++++++++++++++++++ src/util/html.ts | 15 ++++++++++++++ test/unit/table-tests.ts | 11 +++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/util/html.ts diff --git a/src/table.ts b/src/table.ts index 2aab2b7e..51acfae5 100644 --- a/src/table.ts +++ b/src/table.ts @@ -43,6 +43,9 @@ import { DataProps } from './data.js'; import { clampRange, wrapIndex } from './util/vector.js'; import { ArrayDataType, BigIntArray, TypedArray, TypedArrayDataType } from './interfaces.js'; import { RecordBatch, _InternalEmptyPlaceholderRecordBatch } from './recordbatch.js'; +import { escapeHTML } from './util/html.js'; + +const jupyterDisplay = Symbol.for("Jupyter.display"); /** @ignore */ export interface Table { @@ -393,6 +396,46 @@ export class Table { (proto as any)['indexOf'] = wrapChunkedIndexOf(indexOfVisitor.getVisitFn(Type.Struct)); return 'Table'; })(Table.prototype); + + /** + * Render the table as HTML table element. + */ + public toHTML(): string { + let htmlTable = ""; + + // Add table headers + htmlTable += ""; + for (const field of this.schema.fields) { + htmlTable += ``; + } + htmlTable += ""; + // Add table data + htmlTable += ""; + for (const row of this) { + htmlTable += ""; + for (const field of row.toArray()) { + htmlTable += ``; + } + htmlTable += ""; + } + htmlTable += "
${escapeHTML(field.name)}
${escapeHTML(String(field))}
"; + + return htmlTable; + } + + /** + * Jupyter rich content output. + * @see https://docs.deno.com/runtime/reference/cli/jupyter/#rich-content-output + */ + public [jupyterDisplay]() { + // TODO: from env or options + const rows = 50 + const limited = this.slice(0, rows); + return { + // TODO: application/vnd.dataresource+json + "text/html": limited.toHTML() + } + } } diff --git a/src/util/html.ts b/src/util/html.ts new file mode 100644 index 00000000..5f37acf5 --- /dev/null +++ b/src/util/html.ts @@ -0,0 +1,15 @@ +const rawMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +} as const; + + +/** + * Escapes text for safe interpolation into HTML. + */ +export function escapeHTML(str: string) { + return str.replace(/[&<>"']/g, (char) => rawMap[char as keyof typeof rawMap]); +} diff --git a/test/unit/table-tests.ts b/test/unit/table-tests.ts index 3f12905a..efcbc279 100644 --- a/test/unit/table-tests.ts +++ b/test/unit/table-tests.ts @@ -24,7 +24,8 @@ import { Schema, Field, Table, RecordBatch, Vector, builderThroughIterable, Float32, Int32, Dictionary, Utf8, Int8, - tableFromIPC, tableToIPC, vectorFromArray + tableFromIPC, tableToIPC, vectorFromArray, + tableFromJSON } from 'apache-arrow'; const deepCopy = (t: Table) => tableFromIPC(tableToIPC(t)); @@ -306,6 +307,14 @@ describe(`Table`, () => { compareBatchAndTable(table, m, batch2, deepCopy(new Table([batch2]))); }); + test(`table.toHTML() create right HTML table`, () => { + const table = tableFromJSON([{name: "Alice", age: 30}, {name: "Bob", age: 32}]) + const html = table.toHTML() + const expected = `
nameage>
Alice30
Bob32
` + + expect(html).toEqual(expected) + }) + for (const datum of test_data) { describe(datum.name, () => { test(`has the correct length`, () => { From c89249c446ed0a886cf66d897b70674ea60c05f8 Mon Sep 17 00:00:00 2001 From: Xubai Wang <18016038327@189.cn> Date: Tue, 30 Sep 2025 17:12:09 +0800 Subject: [PATCH 2/3] fix: jupyter add dataframe class --- src/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/table.ts b/src/table.ts index 51acfae5..64422ffc 100644 --- a/src/table.ts +++ b/src/table.ts @@ -401,7 +401,7 @@ export class Table { * Render the table as HTML table element. */ public toHTML(): string { - let htmlTable = ""; + let htmlTable = `
`; // Add table headers htmlTable += ""; From a3d921572d6dcd2ba39877f2f434ce6251f3ba0a Mon Sep 17 00:00:00 2001 From: Xubai Wang <18016038327@189.cn> Date: Sun, 5 Oct 2025 13:38:08 +0800 Subject: [PATCH 3/3] refactor: move deno to separate file --- src/Arrow.deno.ts | 21 ++++++++++++ src/table-jupyter.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ src/table.ts | 42 ------------------------ src/util/html.ts | 15 --------- 4 files changed, 98 insertions(+), 57 deletions(-) create mode 100644 src/Arrow.deno.ts create mode 100644 src/table-jupyter.ts delete mode 100644 src/util/html.ts diff --git a/src/Arrow.deno.ts b/src/Arrow.deno.ts new file mode 100644 index 00000000..7aea17bd --- /dev/null +++ b/src/Arrow.deno.ts @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export * from "./Arrow.node.js" + +// Extend table with jupyter display +import "./table-jupyter.js" diff --git a/src/table-jupyter.ts b/src/table-jupyter.ts new file mode 100644 index 00000000..0cffc118 --- /dev/null +++ b/src/table-jupyter.ts @@ -0,0 +1,77 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Table } from "./table.js" + +const jupyterDisplay = Symbol.for("Jupyter.display"); + +const rawMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +} as const; + +/** + * Escapes text for safe interpolation into HTML. + */ +function escapeHTML(str: string) { + return str.replace(/[&<>"']/g, (char) => rawMap[char as keyof typeof rawMap]); +} + +/** + * Render the table as HTML table element. + */ +function tableToHTML(table: Table): string { + let htmlTable = `
`; + + // Add table headers + htmlTable += ""; + for (const field of table.schema.fields) { + htmlTable += ``; + } + htmlTable += ""; + // Add table data + htmlTable += ""; + for (const row of table) { + htmlTable += ""; + for (const field of row.toArray()) { + htmlTable += ``; + } + htmlTable += ""; + } + htmlTable += "
${escapeHTML(field.name)}
${escapeHTML(String(field))}
"; + + return htmlTable; +} + +declare module "./table.js" { + interface Table { + [jupyterDisplay](): { "text/html": string }; + } +} + +Table.prototype[jupyterDisplay] = function(this: Table) { + // TODO: from env or options + const rows = 50 + const limited = this.slice(0, rows); + return { + // TODO: application/vnd.dataresource+json + "text/html": tableToHTML(limited) + } +} diff --git a/src/table.ts b/src/table.ts index 64422ffc..d68e9070 100644 --- a/src/table.ts +++ b/src/table.ts @@ -43,9 +43,7 @@ import { DataProps } from './data.js'; import { clampRange, wrapIndex } from './util/vector.js'; import { ArrayDataType, BigIntArray, TypedArray, TypedArrayDataType } from './interfaces.js'; import { RecordBatch, _InternalEmptyPlaceholderRecordBatch } from './recordbatch.js'; -import { escapeHTML } from './util/html.js'; -const jupyterDisplay = Symbol.for("Jupyter.display"); /** @ignore */ export interface Table { @@ -396,46 +394,6 @@ export class Table { (proto as any)['indexOf'] = wrapChunkedIndexOf(indexOfVisitor.getVisitFn(Type.Struct)); return 'Table'; })(Table.prototype); - - /** - * Render the table as HTML table element. - */ - public toHTML(): string { - let htmlTable = ``; - - // Add table headers - htmlTable += ""; - for (const field of this.schema.fields) { - htmlTable += ``; - } - htmlTable += ""; - // Add table data - htmlTable += ""; - for (const row of this) { - htmlTable += ""; - for (const field of row.toArray()) { - htmlTable += ``; - } - htmlTable += ""; - } - htmlTable += "
${escapeHTML(field.name)}
${escapeHTML(String(field))}
"; - - return htmlTable; - } - - /** - * Jupyter rich content output. - * @see https://docs.deno.com/runtime/reference/cli/jupyter/#rich-content-output - */ - public [jupyterDisplay]() { - // TODO: from env or options - const rows = 50 - const limited = this.slice(0, rows); - return { - // TODO: application/vnd.dataresource+json - "text/html": limited.toHTML() - } - } } diff --git a/src/util/html.ts b/src/util/html.ts deleted file mode 100644 index 5f37acf5..00000000 --- a/src/util/html.ts +++ /dev/null @@ -1,15 +0,0 @@ -const rawMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' -} as const; - - -/** - * Escapes text for safe interpolation into HTML. - */ -export function escapeHTML(str: string) { - return str.replace(/[&<>"']/g, (char) => rawMap[char as keyof typeof rawMap]); -}