Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions pycdfpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,30 @@ def _(attribute: Attribute) -> dict:
}


@to_dict_skeleton.register(VariableAttribute)
def _(attribute: VariableAttribute) -> dict:
"""
to_dict_skeleton builds a dictionary skeleton of the VariableAttribute object for use with json.dumps or similar functions.

A variable attribute holds a single entry, so its skeleton uses the same
shape as a global Attribute with a single-element values/types list.

Parameters
----------
attribute: VariableAttribute
input VariableAttribute object

Returns
-------
dict
dictionary skeleton of the VariableAttribute
"""
return {
"values": [_stringify_time_values(attribute.value, attribute.type())],
"types": [str(attribute.type())],
}


@to_dict_skeleton.register(Variable)
def _(variable: Variable) -> dict:
"""
Expand Down
4 changes: 4 additions & 0 deletions tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@ if node.found()
args: [files('wacdfpp_diff/test.mjs')],
timeout: 60,
)
test('wacdfpp_yaml', node,
args: [files('wacdfpp_yaml/test.mjs')],
timeout: 60,
)
endif
10 changes: 10 additions & 0 deletions tests/python_skeletons/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ def test_can_extrac_skeleton(self):
self.assertIsNotNone(skel)
self.assertEqual(type(skel["attributes"]["tt2000_special_values"]["values"][0][0]), str)

def test_variable_attributes_are_serialized(self):
cdf = make_cdf()
skel = pycdfpp.to_dict_skeleton(cdf)
attrs = skel["variables"]["test_variable"]["attributes"]
self.assertIsNotNone(attrs["attr1"])
self.assertEqual(attrs["attr1"]["values"], [[1, 2, 3]])
self.assertEqual(len(attrs["attr1"]["types"]), 1)
# time-valued variable attributes must be stringified like global ones
self.assertEqual(type(attrs["attr2"]["values"][0][0]), str)


if __name__ == '__main__':
unittest.main()
93 changes: 93 additions & 0 deletions tests/wacdfpp_yaml/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Pure Node test for wacdfpp/cdf-yaml.js — no WASM required.
// node test.mjs
import { toYaml, buildSkeleton, CDF_TYPE_NAMES } from "../../wacdfpp/cdf-yaml.js";

let failures = 0;
function check(name, ok) {
if (ok) console.log(`ok ${name}`);
else { console.error(`FAIL ${name}`); failures += 1; }
}
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);

// --- toYaml: scalars ----------------------------------------------------
check("string plain", toYaml({ a: "hello" }) === "a: hello\n");
check("string with space stays plain", toYaml({ a: "hello world" }) === "a: hello world\n");
check("number-like string quoted", toYaml({ a: "123" }) === 'a: "123"\n');
check("colon string quoted", toYaml({ a: "x: y" }) === 'a: "x: y"\n');
check("comma string quoted", toYaml({ a: "a,b" }) === 'a: "a,b"\n');
check("empty string quoted", toYaml({ a: "" }) === 'a: ""\n');
check("newline string escaped", toYaml({ a: "x\ny" }) === 'a: "x\\ny"\n');
check("reserved word quoted", toYaml({ a: "true" }) === 'a: "true"\n');
check("bool", toYaml({ a: true }) === "a: true\n");
check("null", toYaml({ a: null }) === "a: null\n");
check("integer", toYaml({ a: 42 }) === "a: 42\n");
check("nan -> .nan", toYaml({ a: Number.NaN }) === "a: .nan\n");
check("inf -> .inf", toYaml({ a: Infinity }) === "a: .inf\n");
check("-inf -> -.inf", toYaml({ a: -Infinity }) === "a: -.inf\n");

// --- toYaml: arrays -----------------------------------------------------
check("scalar array -> flow", toYaml({ a: [1, 2, 3] }) === "a: [1, 2, 3]\n");
check("single-scalar array -> flow", toYaml({ a: ["mV"] }) === "a: [mV]\n");
check("empty array -> []", toYaml({ a: [] }) === "a: []\n");
check("typed array -> flow", toYaml({ a: new Float64Array([1, 2]) }) === "a: [1, 2]\n");
check("array-of-array -> block",
toYaml({ a: [[1, 2], "x"] }) === "a:\n - [1, 2]\n - x\n");

// --- toYaml: maps -------------------------------------------------------
check("nested map", toYaml({ a: { b: 1 } }) === "a:\n b: 1\n");
check("empty map -> {}", toYaml({ a: {} }) === "a: {}\n");

// --- toYaml: escaping must produce round-trippable YAML -----------------
// (keys are emitted verbatim historically; values with leading YAML
// indicators or YAML-1.1 number forms must be quoted or they re-parse wrong.)
check("special key quoted", toYaml({ "min: max": 1 }) === '"min: max": 1\n');
check("leading-dash value quoted", toYaml({ a: "- pending" }) === 'a: "- pending"\n');
check("leading-question value quoted", toYaml({ a: "? x" }) === 'a: "? x"\n');
check("underscore-number string quoted", toYaml({ a: "1_000" }) === 'a: "1_000"\n');
check("digit-leading string quoted", toYaml({ a: "3things" }) === 'a: "3things"\n');
check("tilde string quoted", toYaml({ a: "~" }) === 'a: "~"\n');
check("bare dash quoted", toYaml({ a: "-" }) === 'a: "-"\n');

// --- CDF_TYPE_NAMES -----------------------------------------------------
check("type map double", CDF_TYPE_NAMES[45] === "CDF_DOUBLE");
check("type map tt2000", CDF_TYPE_NAMES[33] === "CDF_TIME_TT2000");
check("type map char", CDF_TYPE_NAMES[51] === "CDF_CHAR");

// --- buildSkeleton ------------------------------------------------------
const rawVars = [{
name: "v",
typeName: "CDF_FLOAT",
shape: [3],
isNrv: false,
compression: "none",
attributes: { units: "mV", scale: new Int32Array([1, 2, 3]) },
attributeTypes: { units: 51, scale: 4 },
}];
const rawGlobals = [{ name: "Project", entries: ["THEMIS", [1, 2]], types: [51, 11] }];
const skel = buildSkeleton(rawVars, rawGlobals, { compression: "gzip" });

check("top-level compression", skel.compression === "gzip");
check("top-level key order", eq(Object.keys(skel), ["compression", "attributes", "variables"]));
check("global attr values pass-through", eq(skel.attributes.Project.values, ["THEMIS", [1, 2]]));
check("global attr type names", eq(skel.attributes.Project.types, ["CDF_CHAR", "CDF_UINT1"]));

const v = skel.variables.v;
check("var key order", eq(Object.keys(v), ["type", "shape", "is_nrv", "compression", "attributes"]));
check("var type name", v.type === "CDF_FLOAT");
check("var shape", eq(v.shape, [3]));
check("var is_nrv", v.is_nrv === false);
check("var compression", v.compression === "none");
check("var attr units single-entry values", eq(v.attributes.units.values, ["mV"]));
check("var attr units type", eq(v.attributes.units.types, ["CDF_CHAR"]));
check("var attr scale wrapped single entry",
v.attributes.scale.values.length === 1 && eq(Array.from(v.attributes.scale.values[0]), [1, 2, 3]));
check("var attr scale type", eq(v.attributes.scale.types, ["CDF_INT4"]));

// --- end to end ---------------------------------------------------------
const yaml = toYaml(skel);
check("e2e has variables block", yaml.includes("variables:\n"));
check("e2e var name indented", yaml.includes("\n v:\n"));
check("e2e renders var type name", yaml.includes("type: CDF_FLOAT\n"));
check("e2e renders global char type", yaml.includes("CDF_CHAR"));

process.exit(failures === 0 ? 0 : 1);
26 changes: 25 additions & 1 deletion wacdfpp/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Orchestration: load (file/URL/?url=/drag-drop), hold current CdfFile + model +
// selection, wire search and list selection to re-render.
import { loadModule } from "./wasm.js";
import { rawFromCdfFile, buildModel, filterModel } from "./cdf-model.js";
import { rawFromCdfFile, buildModel, filterModel, VAR_GROUPS } from "./cdf-model.js";
import { renderList, renderDetail, setSelected } from "./render.js";
import { renderPlot } from "./plot.js";
import { openValidation, openValidationBytes } from "./astralint.js";
import { runCompare, setView, setFilter } from "./compare.js";
import { toYaml, buildSkeleton } from "./cdf-yaml.js";

const els = {
fileInput: document.getElementById("fileInput"),
Expand All @@ -23,6 +24,7 @@ const els = {
app: document.querySelector(".app"),
resizer: document.getElementById("resizer"),
validateBtn: document.getElementById("validateBtn"),
exportYamlBtn: document.getElementById("exportYamlBtn"),
modeToggle: document.getElementById("modeToggle"),
compareInputs: document.getElementById("compareInputs"),
compareBar: document.getElementById("compareBar"),
Expand All @@ -42,6 +44,7 @@ let currentModel; // built once per file
let currentUrl = null; // source URL of the loaded file (null for local/drag-drop)
let currentBytes = null; // raw bytes of the loaded file (for the postMessage handoff)
let currentName = null; // file name of the loaded file
let currentCompression = null; // file-level compression (for the YAML skeleton)
let selectedName = null;
let searchQuery = "";
let busy = false;
Expand All @@ -52,6 +55,11 @@ function updateValidate() {
els.validateBtn.title = ready
? "Validate this CDF against ISTP in AstraLint"
: "Load a CDF to validate it against ISTP in AstraLint";
const exportable = !!currentCdf;
els.exportYamlBtn.disabled = !exportable;
els.exportYamlBtn.title = exportable
? "Download a YAML metadata skeleton of this CDF"
: "Load a CDF to export its YAML skeleton";
}

function setStatus(cls, text) { els.status.className = cls; els.statusText.textContent = text; }
Expand Down Expand Up @@ -83,6 +91,7 @@ function inspect(data, name, sourceUrl) {
currentUrl = null;
currentBytes = null;
currentName = null;
currentCompression = null;
updateValidate();
refreshList();
els.detail.innerHTML = `<div class="log-err">ERROR: failed to parse CDF</div>`;
Expand All @@ -98,6 +107,7 @@ function inspect(data, name, sourceUrl) {
currentUrl = sourceUrl ?? null;
currentBytes = data;
currentName = name;
currentCompression = cdf.compression();
updateValidate();
els.fileName.textContent = name;
els.parseTime.textContent = `parsed in ${dt} ms`;
Expand Down Expand Up @@ -161,6 +171,20 @@ els.validateBtn.addEventListener("click", () => {
if (currentUrl) openValidation(currentUrl);
else if (currentBytes) openValidationBytes(currentBytes, currentName ?? "file.cdf");
});
els.exportYamlBtn.addEventListener("click", () => {
if (!currentModel) return;
// Build from the already-extracted model — no WASM call, no value load.
const rawVars = VAR_GROUPS.flatMap((g) => currentModel.groups[g]);
const yaml = toYaml(buildSkeleton(rawVars, currentModel.globalAttributes,
{ compression: currentCompression }));
const base = (currentName ?? "cdf").replace(/\.cdf$/i, "");
const blob = new Blob([yaml], { type: "application/yaml" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${base}.skeleton.yaml`;
a.click();
URL.revokeObjectURL(a.href);
});

// Reverse handoff: load a CDF handed to us by an opener (e.g. AstraLint's
// "Open in CDFpp Explorer"). We post 'cdfpp-ready' after init (see init()).
Expand Down
8 changes: 7 additions & 1 deletion wacdfpp/cdf-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,21 @@ export function rawFromCdfFile(cdf) {
const rawVars = cdf.variable_names().map(name => {
const v = cdf.get_variable(name);
const attributes = {};
for (const an of v.attribute_names) attributes[an] = v.attributes[an];
const attributeTypes = {};
for (const an of v.attribute_names) {
attributes[an] = v.attributes[an];
attributeTypes[an] = v.attribute_types[an];
}
return {
name,
shape: Array.from(v.shape),
typeName: v.type_name,
type: v.type,
isNrv: v.is_nrv,
compression: v.compression,
varType: attributes.VAR_TYPE,
attributes,
attributeTypes,
};
});
const rawGlobals = cdf.attribute_names().map(name => {
Expand Down
Loading
Loading