plaintext search
This commit is contained in:
parent
9974f85e6d
commit
4b4d1dbd8a
@ -36,6 +36,6 @@
|
||||
"patch-package": "7.0.0",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "4.3.5",
|
||||
"vite-plugin-pwa": "0.14.7"
|
||||
"vite-plugin-pwa": "0.14.7"
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100vw;
|
||||
html, body {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@ -34,7 +34,7 @@ th {
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
max-width: 100%;
|
||||
position: sticky;
|
||||
position: sticky;
|
||||
box-shadow: 0 -5px 15px 0 #ced4da;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
|
139
src/App.tsx
139
src/App.tsx
@ -1,20 +1,22 @@
|
||||
/* TODO:
|
||||
- history
|
||||
- find example and correctly handle "merge" messages
|
||||
- messages referencing selected msg
|
||||
- sort and filter table
|
||||
- loading icons
|
||||
- expand referenced object in place
|
||||
- paste bytes -> analyze
|
||||
- edit fields / files?
|
||||
- different menu for message / enum / extend / literal
|
||||
- display integers in base16 and base10
|
||||
- search bytes?
|
||||
- highlight results in inspector
|
||||
*/
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { MouseEvent as ReactMouseEvent, ChangeEventHandler, ChangeEvent } from 'react';
|
||||
import './App.css';
|
||||
import { ObjectInspector, ObjectLabel, ObjectName, ObjectValue } from 'react-inspector';
|
||||
import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { process, read, ParsedFile } from './iwa';
|
||||
import { process, read, ParsedFile, TableItem } from './iwa';
|
||||
import { parse_protos, ProtoMap } from './messages';
|
||||
import type { $_TSP_MessageInfo, $_TSP_Reference } from './messages';
|
||||
import { Menu, Item, Separator, useContextMenu } from 'react-contexify';
|
||||
@ -56,11 +58,12 @@ type TableViewProps = {
|
||||
id?: string;
|
||||
data: any[];
|
||||
cols: string[];
|
||||
filter?: (row: any, R: number) => boolean;
|
||||
rowclick?: (row: any, R: number, e: ReactMouseEvent<HTMLTableRowElement, MouseEvent>) => void;
|
||||
cellclick?: (value: any, R: number, C: number, e: ReactMouseEvent<HTMLTableCellElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
function TableView({id, data, cols, rowclick, cellclick}: TableViewProps) {
|
||||
function TableView({id, data, cols, filter, rowclick, cellclick}: TableViewProps) {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
@ -68,7 +71,7 @@ function TableView({id, data, cols, rowclick, cellclick}: TableViewProps) {
|
||||
<th key={idx}>{c}</th>
|
||||
))}</tr>
|
||||
</thead>
|
||||
<tbody>{data.map((row, R) => (
|
||||
<tbody>{data.filter(filter || (()=>true)).map((row, R) => (
|
||||
<tr id={`tr-${R}`} key={R}
|
||||
{...(rowclick ? {onClick: (e) => {e.preventDefault(); e.stopPropagation(); rowclick(row, R, e)}} : {})}
|
||||
{...(row["id"] == id ? {style: {backgroundColor: "#646cff", color: "#FFFFFF" }} : {})}
|
||||
@ -120,6 +123,8 @@ function App() {
|
||||
const [obj, setObj] = useState<any>({});
|
||||
/* current meta */
|
||||
const [meta, setMeta] = useState<$_TSP_MessageInfo>({} as any);
|
||||
/* current deps */
|
||||
const [deps, setDeps] = useState<any[]>([]);
|
||||
/* protobuf definitions */
|
||||
const [protos, setProtos] = useState<ProtoMap>({});
|
||||
/* selected message type */
|
||||
@ -128,6 +133,10 @@ function App() {
|
||||
const [__html, setProto] = useState<string>("Select a Row");
|
||||
/* "dirty" if inspecting a subfield */
|
||||
const [dirty, setDirty] = useState<boolean>(false);
|
||||
/* loading */
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
/* search */
|
||||
const [search, setSearch] = useState<string>("");
|
||||
/* history stack */
|
||||
const [stack, setStack] = useState<string[]>([]);
|
||||
/* react-contexify */
|
||||
@ -138,12 +147,41 @@ function App() {
|
||||
const [menuId, setMenuId] = useState<string>("");
|
||||
const tblRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* parse if message has not been parsed */
|
||||
const preparse = (id: any, message: string, f: ParsedFile = file, p: ProtoMap = protos) => {
|
||||
const item = f.space[+id][0];
|
||||
if(!item.parsed) {
|
||||
item.parsed = process(item.data, message, p);
|
||||
item.pre = JSON.stringify(item.parsed, (_,v) => typeof v == "bigint" ? v.toString() : v instanceof Uint8Array ? [...v]: v);
|
||||
}
|
||||
if(!item.parsedmeta) {
|
||||
const m: $_TSP_MessageInfo = item.parsedmeta = process(item.rawmeta, ".TSP.MessageInfo", p);
|
||||
/* .TSP.MessageInfo */
|
||||
if(m.object_references) m.$object_references = m.object_references.map((n: BigInt) => {
|
||||
/* create a fake reference for the inspector */
|
||||
var o: $_TSP_Reference = ({ identifier: n });
|
||||
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
||||
return o;
|
||||
});
|
||||
if(m.field_infos) m.field_infos.forEach((fi) => {
|
||||
/* .TSP.FieldInfo */
|
||||
if(fi.object_references) fi.$object_references = fi.object_references.map((n: BigInt) => {
|
||||
/* create a fake reference for the inspector */
|
||||
var o: $_TSP_Reference = ({ identifier: n });
|
||||
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
||||
return o;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
/* update selection based on table row */
|
||||
const doitRow = (row: any, R: number, reset?: boolean) => {
|
||||
const doitRow = (row: TableItem, R: number, reset?: boolean) => {
|
||||
let obj: any, meta: $_TSP_MessageInfo;
|
||||
try {
|
||||
obj = process(file.space[+row.id][0].data, row.message, protos);
|
||||
meta = process(file.space[+row.id][0].rawmeta, ".TSP.MessageInfo", protos);
|
||||
preparse(row.id, row.message);
|
||||
const item = file.space[+row.id][0];
|
||||
obj = item.parsed;
|
||||
meta = item.parsedmeta as $_TSP_MessageInfo;
|
||||
} catch(e) {
|
||||
console.error(row, e); toast.error(`Could not parse ${row.id} (${row.type})`, {position: toast.POSITION.TOP_CENTER}); return;
|
||||
}
|
||||
@ -152,23 +190,11 @@ function App() {
|
||||
setProto(protos[row.message] || "");
|
||||
setDirty(false);
|
||||
setObj(obj);
|
||||
/* .TSP.MessageInfo */
|
||||
if(meta.object_references) meta.$object_references = meta.object_references.map((n: BigInt) => {
|
||||
/* create a fake reference for the inspector */
|
||||
var o: $_TSP_Reference = ({ identifier: n });
|
||||
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
||||
return o;
|
||||
});
|
||||
if(meta.field_infos) meta.field_infos.forEach((fi) => {
|
||||
/* .TSP.FieldInfo */
|
||||
if(fi.object_references) fi.$object_references = fi.object_references.map((n: BigInt) => {
|
||||
/* create a fake reference for the inspector */
|
||||
var o: $_TSP_Reference = ({ identifier: n });
|
||||
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
||||
return o;
|
||||
});
|
||||
})
|
||||
setMeta(meta);
|
||||
setDeps(file.tbl.filter(r => ((file.space[+r.id][0].parsedmeta?.object_references||[])?.indexOf(BigInt(row.id)) > -1)).map(v => {
|
||||
/* this copy uses non-enumerable fields */
|
||||
var o = {}; Object.entries(v).forEach(([k,v])=> Object.defineProperty(o, k, { enumerable: false, value: v })); return o;
|
||||
}));
|
||||
setId(String(row.id));
|
||||
var rowelt = document.getElementById(`tr-${R}`);
|
||||
var top = rowelt?.offsetTop || 0;
|
||||
@ -196,29 +222,56 @@ function App() {
|
||||
doitRow(file.tbl[R], R);
|
||||
};
|
||||
|
||||
/* on load, get protobuf definitions and process the test file */
|
||||
/* filter message table */
|
||||
const filter = (row: any) => {
|
||||
if(!search) return true;
|
||||
preparse(row.id, row.message);
|
||||
return (file.space[+row.id][0].pre||"").toLowerCase().indexOf(search.toLowerCase()) > -1;
|
||||
//return row.message == ".TN.SheetArchive";
|
||||
}
|
||||
|
||||
/* load file */
|
||||
const process_ab = (ab: ArrayBuffer, p: ProtoMap = protos) => {
|
||||
const f = read(ab);
|
||||
f.tbl.forEach(row => preparse(row.id, row.message, f, p));
|
||||
setFile(f);
|
||||
};
|
||||
|
||||
/* on page load, get protobuf definitions and process the test file */
|
||||
useEffect(() => {
|
||||
const id = toast.loading(`Processing test.numbers`);
|
||||
let ignore = false;
|
||||
(async () => {
|
||||
await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine
|
||||
const protos = await (await fetch("protos")).text();
|
||||
const testfile = await (await fetch("test.numbers")).arrayBuffer();
|
||||
if(ignore) return;
|
||||
setProtos(parse_protos(protos));
|
||||
setFile(read(testfile));
|
||||
const p = parse_protos(protos);
|
||||
setProtos(p);
|
||||
process_ab(testfile, p);
|
||||
toast.dismiss(id);
|
||||
setLoading(false);
|
||||
})();
|
||||
return () => { ignore = true; }
|
||||
return () => { ignore = true; toast.dismiss(id); }
|
||||
}, []);
|
||||
useEffect(() => { if(file.tbl[0]) doitRow(file.tbl[0], 0, true); }, [file]);
|
||||
|
||||
const onChange: ChangeEventHandler<HTMLInputElement> = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); }
|
||||
let data: ParsedFile;
|
||||
try { data = read(await e.target.files?.[0].arrayBuffer()); } catch(e) {
|
||||
setLoading(true);
|
||||
if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); setLoading(false); }
|
||||
try {
|
||||
const id = toast.loading(`Processing ${e.target.files[0].name}`);
|
||||
await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine
|
||||
process_ab(await e.target.files?.[0].arrayBuffer());
|
||||
toast.dismiss(id);
|
||||
} catch(e) {
|
||||
if((e as any).message?.includes("Failed to read archive")) toast.error("Please select an iWork file", { position: toast.POSITION.TOP_CENTER });
|
||||
else toast.error(e && (e as any).message || e, { position: toast.POSITION.TOP_CENTER });
|
||||
console.error(e); return;
|
||||
console.error(e);
|
||||
} finally {
|
||||
e.target.value = "";
|
||||
setLoading(false);
|
||||
}
|
||||
setFile(data);
|
||||
}
|
||||
|
||||
/* Menu machinations */
|
||||
@ -297,28 +350,35 @@ function App() {
|
||||
}
|
||||
return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
|
||||
};
|
||||
const depsRenderer = ({ depth, data, isNonenumerable }: NodeRendererProps) => {
|
||||
if(depth === 0) return ( <b>Dependents</b> );
|
||||
return ( <a onClick={()=>gotoRef(data.id)}><ObjectLabel name={data.id} data={data.message} isNonenumerable={isNonenumerable} /></a> );
|
||||
};
|
||||
|
||||
return ( <>
|
||||
{/* header */}
|
||||
<div id="header" className="header"><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector <input type="file" id="file" onChange={onChange} /></b></div>
|
||||
<div id="header" className="header" style={{display: "grid", gridTemplateColumns: "repeat(3, 1fr)", alignItems:"center"}}>
|
||||
<div style={{display: "flex", justifyContent: "flex-start"}}><input type="file" id="file" onChange={onChange} disabled={loading} accept=".numbers,.key,.pages"/></div>
|
||||
<div><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector</b></div>
|
||||
<div style={{display: "flex", justifyContent: "flex-end"}}><input type="text" value={search} placeholder="search for text" onChange={(e)=>setSearch(e.target.value)}/></div>
|
||||
</div>
|
||||
|
||||
<div className="page">
|
||||
<PanelGroup direction='vertical'>
|
||||
<Panel defaultSize={25}><div className="overflow" ref={tblRef}>
|
||||
|
||||
{/* message table */}
|
||||
<TableView data={file.tbl} cols={["id", "type", "message", "path"]} id={id} rowclick={rowclick} />
|
||||
<TableView data={file.tbl} cols={["id", "type", "message", "path"]} filter={filter} id={id} rowclick={rowclick} />
|
||||
|
||||
</div></Panel>
|
||||
<PanelResizeHandle style={{ height: "3px", backgroundColor: "#EEEEEE" }} />
|
||||
<Panel><div className="overflow">
|
||||
|
||||
<Panel><div className="overflow" style={{display: "grid", gridTemplateColumns: "1fr", gridTemplateRows: "max-content 1fr"}}>
|
||||
{/* selected message bar */}
|
||||
<div style={{boxShadow: "0 2px 2px -1px rgba(0, 0, 0, 0.4)", height: "24px"}}>{sel ? (<>
|
||||
<b>Selected message {(stack.length > 4 ? [...stack.slice(0,2), "...", ...stack.slice(-1)] : stack).map(i => i + " > ").join("")} {id} ({sel}) {file.space[+id]?.[0]?.data?.length || 0} bytes {stack.length && <a onClick={pop}>Return to {stack[stack.length - 1]}</a> || ""}</b>
|
||||
</>) : "Select a message to see the contents"}</div>
|
||||
|
||||
<div style={{width: "100%", height: "calc(100% - 24px)", overflow: "auto"}}><PanelGroup direction='horizontal'>
|
||||
<div className='overflow'><PanelGroup direction='horizontal'>
|
||||
<Panel><div className='overflow'>
|
||||
|
||||
{/* proto definition */}
|
||||
@ -333,13 +393,14 @@ function App() {
|
||||
|
||||
</div></Panel>
|
||||
<PanelResizeHandle style={{ width: "2px", backgroundColor: "#EEEEEE" }} />
|
||||
<Panel><div className="overflow" style={{textAlign: "left", marginTop: "13px", marginLeft: "10px", marginBottom: "13px", "paddingBottom": "12px"}}>
|
||||
<Panel><div className="overflow" style={{textAlign: "left", marginTop: "5px", paddingLeft: "5px"}}>
|
||||
|
||||
{/* inspector */}
|
||||
<b>Message</b>
|
||||
<ObjectInspector data={obj} expandLevel={1} nodeRenderer={nodeRenderer} />
|
||||
<b>Meta</b>
|
||||
<ObjectInspector data={meta} expandLevel={1} nodeRenderer={metaRenderer} />
|
||||
{deps.length && <ObjectInspector data={deps} expandLevel={1} nodeRenderer={depsRenderer} /> || void 0}
|
||||
<div style={{height:"13px"}}></div>
|
||||
|
||||
</div></Panel>
|
||||
@ -356,4 +417,4 @@ function App() {
|
||||
</> );
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as CFB from 'cfb';
|
||||
|
||||
import Messages, { MessageTypes } from "./messages/";
|
||||
import Messages, { $_TSP_MessageInfo, MessageTypes } from "./messages/";
|
||||
|
||||
/* see https://bugs.webkit.org/show_bug.cgi?id=243148 -- affects iOS Safari */
|
||||
declare var Buffer: any; // Buffer is typeof-guarded but TS still needs this :(
|
||||
@ -119,6 +119,9 @@ interface IWAMessage {
|
||||
meta: ProtoMessage;
|
||||
rawmeta: Uint8Array;
|
||||
data: Uint8Array;
|
||||
pre?: string;
|
||||
parsed?: any;
|
||||
parsedmeta?: $_TSP_MessageInfo;
|
||||
}
|
||||
interface IWAArchiveInfo {
|
||||
id: number;
|
||||
|
@ -29,6 +29,7 @@ const post_process = (buf: string, key: string, out: ProtoMap) => {
|
||||
return $1 + " " + key + "." + $2
|
||||
});
|
||||
out[new_key] = payload;
|
||||
post_process(payload, new_key, out);
|
||||
payload = "";
|
||||
} else if(nested) payload += row + "\n";
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user