From b6abcc835d79d23d9223c05b144c8940bea4125a Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 21:21:36 +0200 Subject: [PATCH 1/6] draft: a page of a kind its section lacks borrows a model of the same shape from the group's largest section that has one --- WIP.Indexer.md | 6 ++ indexer/README.md | 6 +- indexer/lib/draft-plan.mjs | 129 +++++++++++++++++++++++++++---------- indexer/twin-index.mjs | 26 +++++++- test/indexer.test.mjs | 38 +++++++++++ 5 files changed, 170 insertions(+), 35 deletions(-) diff --git a/WIP.Indexer.md b/WIP.Indexer.md index 71b36f37..99f4ccb8 100644 --- a/WIP.Indexer.md +++ b/WIP.Indexer.md @@ -220,6 +220,12 @@ the old tool's at BETA 997, and it takes half a second with no cache. folder of the type returning it is limited to folders that already keep a type, because without that limit it moved Printer under Printers, Dictionary under JsonConverter and eight others that the sections keep at their roots. +- **A new section borrows its model pages from its group.** Its pages have no sibling of + their kind to follow, so the plan of the first package drafted into a new section + (tbRegExp) gave every task `model: null`, and the agents would have invented a page + layout. A page of a kind its section lacks now takes a model from the group's largest + section that has one, of the same shape: a folder's `index.md` for a class that gets a + folder, never a one-page class. - **A permalink is computed only where the rule holds for every existing page** of the section (`followsFolders`). On staging at BETA 997 that is every section but VBA's, whose pages live under `/tB/Modules/`. A section that breaks the rule once gets no computed diff --git a/indexer/README.md b/indexer/README.md index 6f594fad..9afc3a93 100644 --- a/indexer/README.md +++ b/indexer/README.md @@ -243,7 +243,11 @@ pages and the package's `api.json` and decides, for each task: - **the permalink**, only when every page of the section has the permalink its folders give it (`followsFolders`); otherwise null, to follow the siblings. VBA's `/tB/Modules/...` is the one section that does not. -- **a model page** of the same kind in the section, and the source file. +- **a model page** of the same kind in the section, and the source file. A page + of a kind the section has none of, as every page of a new section is, takes + its model from another section of the group (`sectionModels`), the one with the + most pages first, and one of the same shape (a folder's `index.md`, or a page + of its own) where there is one. A package with no section gets `docs/Reference///` and a landing page like the group's others (`newLandingFor`): ` Package`, diff --git a/indexer/lib/draft-plan.mjs b/indexer/lib/draft-plan.mjs index 20f8f9c5..c7a4ba2c 100644 --- a/indexer/lib/draft-plan.mjs +++ b/indexer/lib/draft-plan.mjs @@ -80,6 +80,69 @@ function mostCommon(values, fallback) { return best; } +// The page of each of a section's types that has one, by the type's path, and +// how to find a symbol's page: the one titled with its name whose parent is its +// type's page; for a type of the package's own, the first so titled. A control +// (a class marked [WindowsControl] or [CustomControl]) is a kind of its own +// (`kindKey`): the sections lay their controls out apart from their classes. +function documentedTypes({ api, pages, landing }) { + const byTitle = new Map(); + for (const p of pages) { + if (p.rel === "index.md") continue; + if (!byTitle.has(p.title)) byTitle.set(p.title, []); + byTitle.get(p.title).push(p); + } + const pageOf = (name, host, isTopType) => { + const found = byTitle.get(name) ?? []; + return found.find((p) => host && p.parent === host.title) ?? (isTopType ? found[0] : null) ?? null; + }; + const symbols = flattenSymbols(api ?? {}); + const types = symbols + .filter((s) => s.isType) + .map((s) => ({ ...s, path: s.container ? `${s.container}.${s.name}` : s.name })); + const controls = new Set( + types + .filter((t) => JSON.parse(t.attributes).some((a) => a.name === "WindowsControl" || a.name === "CustomControl")) + .map((t) => t.path), + ); + const kindKey = (kind, typePath) => (controls.has(typePath) ? `${kind} control` : kind); + const typePages = new Map(); + for (const t of types) { + const page = t.container ? pageOf(t.name, typePages.get(t.container), false) : pageOf(t.name, landing, true); + if (page) typePages.set(t.path, page); + } + return { symbols, types, kindKey, typePages, pageOf }; +} + +/** + * The pages a section offers another section of its group as models, for the + * kinds that one has no page of: a Map from each kind of type (`Class`, + * `Class control`, `Enum`...) to the pages documenting one, and from each kind + * of member (`member Property`) to the pages of their own its members have, + * `rel` to the section's folder, in the order of the api. `api` and `pages` are + * as `planPackage` takes them. + */ +export function sectionModels({ api, pages }) { + const landing = pages.find((p) => p.rel === "index.md") ?? null; + const { symbols, types, kindKey, typePages, pageOf } = documentedTypes({ api, pages, landing }); + const models = new Map(); + const add = (key, rel) => { + if (!models.has(key)) models.set(key, []); + if (!models.get(key).includes(rel)) models.get(key).push(rel); + }; + for (const t of types) { + const page = typePages.get(t.path); + if (page) add(kindKey(t.kind, t.path), page.rel); + } + for (const s of symbols) { + if (s.isType || !s.public || !s.container) continue; + const host = typePages.get(s.container); + const page = host ? pageOf(s.name, host, false) : null; + if (page && page.rel !== host.rel) add(`member ${s.kind}`, page.rel); + } + return models; +} + /** * Plans the pages of one package's report. * @@ -94,6 +157,10 @@ function mostCommon(values, fallback) { * - `newLanding`: for a section with no landing page yet, the `{ title, * parent, permalink }` it is to get, and for a contributed package its * `author`; the returned `landing` then says `new: true`. + * - `borrowedModels`: the other sections of the group, most preferred first, + * each `{ dir, models }`, `dir` relative to the repository and `models` from + * `sectionModels`; a page gets its model from them when its section has no + * page of its kind, as a new section has none. * * Returns `{ package, group, mode, version, sectionDir, landing, permalinks, * tasks, unplaced, flagged }`. Each of `tasks` names the page `file` @@ -101,12 +168,13 @@ function mostCommon(values, fallback) { * on the one page its type has; the `parent` title; the `permalink` (null for * an inline symbol, and when the section's permalinks do not follow its * folders); the `containerPage`, which may list its members; a `model` page of - * the same kind in the section, to follow; and the `source` file to read. + * the same kind in the section, else in another section of the group, to + * follow; and the `source` file to read. * `unplaced` are the tasks no page can be found for, such as a member whose * type has no page and gets none; `flagged`, the symbols the package no longer * declares, with the page that documents each. */ -export function planPackage({ report, api, pages, sectionDir, sourceDir, newLanding = null }) { +export function planPackage({ report, api, pages, sectionDir, sourceDir, newLanding = null, borrowedModels = [] }) { const landing = pages.find((p) => p.rel === "index.md") ?? (newLanding ? { rel: "index.md", new: true, ...newLanding } : null); const permalinks = followsFolders(pages, landing); @@ -115,46 +183,21 @@ export function planPackage({ report, api, pages, sectionDir, sourceDir, newLand // The pages there are, and those planned so far. const rels = new Set(pages.map((p) => p.rel)); const folderOf = (rel) => memberFolder(rel, rels); - const byTitle = new Map(); - for (const p of pages) { - if (p.rel === "index.md") continue; - if (!byTitle.has(p.title)) byTitle.set(p.title, []); - byTitle.get(p.title).push(p); - } - // A symbol's page: the one titled with its name whose parent is its type's - // page; for a type of the package's own, the first so titled. - const pageOf = (name, host, isTopType) => { - const found = byTitle.get(name) ?? []; - return found.find((p) => host && p.parent === host.title) ?? (isTopType ? found[0] : null) ?? null; - }; + const { symbols, types, kindKey, typePages, pageOf } = documentedTypes({ api, pages, landing }); - // The page of each type that has one, by the type's path; for each kind of - // type, the folders the section keeps it in, and whether each is a folder of - // its own (`X/index.md`) or one page (`X.md`); and a page of each kind. A - // control (a class marked [WindowsControl] or [CustomControl]) is a kind of - // its own: the sections lay their controls out apart from their classes. - const typePages = new Map(); + // For each kind of type, the folders the section keeps it in, and whether + // each is a folder of its own (`X/index.md`) or one page (`X.md`); and a page + // of each kind. const kindFolders = new Map(); const kindStyles = new Map(); const kindPages = new Map(); - const symbols = flattenSymbols(api ?? {}); - const types = symbols - .filter((s) => s.isType) - .map((s) => ({ ...s, path: s.container ? `${s.container}.${s.name}` : s.name })); - const controls = new Set( - types - .filter((t) => JSON.parse(t.attributes).some((a) => a.name === "WindowsControl" || a.name === "CustomControl")) - .map((t) => t.path), - ); - const kindKey = (kind, typePath) => (controls.has(typePath) ? `${kind} control` : kind); const add = (map, key, value) => { if (!map.has(key)) map.set(key, []); map.get(key).push(value); }; for (const t of types) { - const page = t.container ? pageOf(t.name, typePages.get(t.container), false) : pageOf(t.name, landing, true); + const page = typePages.get(t.path); if (!page) continue; - typePages.set(t.path, page); const key = kindKey(t.kind, t.path); if (!kindPages.has(key)) kindPages.set(key, page.rel); if (t.container) continue; @@ -166,6 +209,20 @@ export function planPackage({ report, api, pages, sectionDir, sourceDir, newLand const byKind = (map, kind, typePath) => map.get(kindKey(kind, typePath)) ?? map.get(kind); const folderFor = (kind, typePath) => mostCommon(byKind(kindFolders, kind, typePath) ?? [], anyFolder); const modelFor = (kind, typePath) => kindPages.get(kindKey(kind, typePath)) ?? kindPages.get(kind) ?? null; + // A model from another section of the group, for a page of a kind this one + // has none of: one of the same shape (a folder's index.md, or a page of its + // own) where a section has one, else any, in the order the sections are given. + const isFolderPage = (rel) => rel === "index.md" || rel.endsWith("/index.md"); + const borrow = (key, rel) => { + let any = null; + for (const { dir, models } of borrowedModels) { + for (const m of models.get(key) ?? []) { + if (isFolderPage(m) === isFolderPage(rel)) return `${dir}/${m}`; + any ??= `${dir}/${m}`; + } + } + return any; + }; // Whether a new type of a kind gets a folder: as most of the section's types // of that kind have, else most of its types; null when it has none. const folderStyle = (kind, typePath) => @@ -293,6 +350,12 @@ export function planPackage({ report, api, pages, sectionDir, sourceDir, newLand } if (isType) typePages.set(ownPath, { rel, title: task.symbol, parent }); rels.add(rel); + let modelFile = model && model !== rel ? inSection(model) : null; + if (!modelFile && !inline) { + modelFile = isType + ? (borrow(kindKey(task.kind, ownPath), rel) ?? borrow(task.kind, rel)) + : borrow(`member ${task.kind}`, rel); + } tasks.push({ ...common(task), file: inSection(rel), @@ -301,7 +364,7 @@ export function planPackage({ report, api, pages, sectionDir, sourceDir, newLand parent, permalink: permalinks && !inline ? permalinkByFolder(landing.permalink, rel, rels) : null, containerPage: host ? inSection(host.rel) : null, - model: model && model !== rel ? inSection(model) : null, + model: modelFile, }); } diff --git a/indexer/twin-index.mjs b/indexer/twin-index.mjs index babeb7d1..55c197c4 100644 --- a/indexer/twin-index.mjs +++ b/indexer/twin-index.mjs @@ -34,7 +34,7 @@ import { indexerFingerprint } from "./lib/fingerprint.mjs"; import { diffApi, auditCoverage } from "./lib/differ.mjs"; import { documentedPackages, groupFolder, sectionPages } from "./lib/doc-pages.mjs"; import { derivePackageApi } from "./lib/package-api.mjs"; -import { batchTasks, newLandingFor, planPackage } from "./lib/draft-plan.mjs"; +import { batchTasks, newLandingFor, planPackage, sectionModels } from "./lib/draft-plan.mjs"; exitOnCrash(); @@ -805,6 +805,29 @@ async function runDraft(filter) { const rel = (p) => path.relative(REPO_ROOT, p).split(path.sep).join("/"); const writeJson = (file, value) => fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`); + // The model pages each section of a group offers the others, the most + // documented section first, for the pages whose section has none of their kind. + const modelsByGroup = new Map(); + const groupModels = async (group) => { + if (modelsByGroup.has(group)) return modelsByGroup.get(group); + const found = []; + for (const [key, section] of documented) { + const [g, name] = key.split("/"); + if (g !== group) continue; + let api; + try { + api = JSON.parse(await fs.readFile(path.join(STORE_PATH, group, name, "api.json"), "utf-8")); + } catch { + continue; // not in the store + } + const pages = await sectionPages(section.dir); + found.push({ dir: rel(section.dir), size: pages.length, models: sectionModels({ api, pages }) }); + } + found.sort((a, b) => b.size - a.size || a.dir.localeCompare(b.dir)); + modelsByGroup.set(group, found); + return found; + }; + const manifest = { packages: [] }; let batchNo = 0; for (const report of chosen) { @@ -837,6 +860,7 @@ async function runDraft(filter) { sectionDir: rel(sectionDir), sourceDir: rel(path.join(storeDir, "sources")), newLanding, + borrowedModels: (await groupModels(report.group)).filter((s) => s.dir !== rel(sectionDir)), }); const planFile = path.join(outDir, `${report.package}.json`); await writeJson(planFile, plan); diff --git a/test/indexer.test.mjs b/test/indexer.test.mjs index 1c70e363..b5b25102 100644 --- a/test/indexer.test.mjs +++ b/test/indexer.test.mjs @@ -42,6 +42,7 @@ import { PAGES_PER_BATCH, permalinkByFolder, planPackage, + sectionModels, } from "../indexer/lib/draft-plan.mjs"; import { FINGERPRINTED, fingerprintOf, indexerFingerprint } from "../indexer/lib/fingerprint.mjs"; import { derivePackageApi } from "../indexer/lib/package-api.mjs"; @@ -1696,6 +1697,43 @@ describe("planning the pages of a report", () => { assert.equal(t.model, "docs/Toy/Enumerations/Shade.md"); }); + test("a page of a kind its section has none of takes a model of the same shape from another section", () => { + const models = sectionModels({ api, pages }); + assert.deepEqual(models.get("Class"), ["Widget/index.md"]); + assert.deepEqual(models.get("Enum"), ["Enumerations/Shade.md"]); + assert.deepEqual(models.get("Module"), ["Helpers.md"]); + assert.deepEqual(models.get("member Property"), ["Widget/Size.md"]); + assert.equal(models.has("member Sub"), false); + + const fresh = planPackage({ + report: { + ...report, + tasks: [ + task("create", "Gadget", "Class", "Gadget"), + task("create", "Spin", "Sub", "Gadget"), + task("create", "Hue", "Enum", "Hue"), + ], + }, + api, + pages: [], + sectionDir: "docs/New", + sourceDir: "s", + newLanding: newLandingFor("New", []), + borrowedModels: [ + { dir: "docs/Flat", models: new Map([["Class", ["Flat.md"]]]) }, + { dir: "docs/Toy", models }, + ], + }); + const model = (symbol) => fresh.tasks.find((t) => t.symbol === symbol).model; + // Gadget gets a folder, so Flat.md, a page of its own, is passed over. + assert.equal(model("Gadget"), "docs/Toy/Widget/index.md"); + assert.equal(model("Hue"), "docs/Toy/Enumerations/Shade.md"); + // No section has a Sub on a page of its own. + assert.equal(model("Spin"), null); + // A section's own model comes first. + assert.equal(of("Hue").model, "docs/Toy/Enumerations/Shade.md"); + }); + test("a new type with new members gets a folder, and its members pages in it", () => { assert.equal(of("Gadget").file, "docs/Toy/Gadget/index.md"); assert.equal(of("Gadget").parent, "Toy Package"); From 1d588dfbc8215c6e6099916b856f29ef64f576be Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 21:23:42 +0200 Subject: [PATCH 2/6] sync: a documented package with no snapshot gets the store's api.json when it is of the section's indexed_from version --- WIP.Indexer.md | 9 ++++----- docs/Documentation/Tools-Build.md | 2 +- indexer/README.md | 5 ++++- indexer/lib/store.mjs | 19 +++++++++++++++++++ indexer/twin-index.mjs | 6 ++++++ test/indexer.test.mjs | 25 +++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 7 deletions(-) diff --git a/WIP.Indexer.md b/WIP.Indexer.md index 99f4ccb8..a99e56bc 100644 --- a/WIP.Indexer.md +++ b/WIP.Indexer.md @@ -135,7 +135,10 @@ which would write the snapshots of another build. - **Snapshots are committed only for the documented packages, to start with**: 14 files, 3.2 MB (about 350 KB compressed). The contributed packages and the extra CEF builds are indexed into the store and left out of the repository. A package joins when its section does; `sync` - copies a snapshot for a documented package it indexed in that run. + copies a snapshot for a documented package it indexed in that run, and gives a documented + package with none the store's `api.json` when that is of its `indexed_from` version + (`adoptSnapshots`): a package is usually synced before its section exists, and a later + sync finds its store current and indexes nothing. - **They are taken from the build the reference was last indexed against, BETA 997** (`beta-x-0997`), not from a newer one, so that the pages and the baseline agree and `analyze` of a newer build shows the real distance. @@ -266,10 +269,6 @@ the old tool's at BETA 997, and it takes half a second with no cache. `Assert` functions on `Exact.md`) shows as undocumented, and a name shared by several types shows as documented once any page carries it. Update mode does not audit, but finds a changed symbol's `doc_page` the same way. -- **Adopting a package whose store is already current writes no snapshot.** By reading `runSync`: - the snapshots are copied for the packages the run indexed, and a package that is up to date - is not among them. Documenting a package that `sync` has already read needs its store folder - removed first, or a build newer than the store's. - **`indexer/TWINSERV-protocol.md` stays where it is, as reference beside the code.** It describes a third party's HTTP API, which is neither a reason nor a decision, and folding 239 lines of it into this note would bury both. Its one reader is `lib/twinserv-client.mjs`, which diff --git a/docs/Documentation/Tools-Build.md b/docs/Documentation/Tools-Build.md index 035cbd45..78ad5e24 100644 --- a/docs/Documentation/Tools-Build.md +++ b/docs/Documentation/Tools-Build.md @@ -188,7 +188,7 @@ A name that matches no package is reported, the other packages are done, and the `sync` judges a package by what the store holds, never by the manifest. A package is read again when the store lacks its sources or its `api.json`, or holds an `api.json` in an older format or from another build. A package whose `api.json` another version of the scanner wrote is indexed again from the sources already in the store, with no download. A clone has no store, so its first `sync` reads every package. -**Which packages are documented.** A section of the reference documents a package when its folder under `docs/Reference/Default/`, `Built-In/` or `Contributed/` has an `index.md`, and the landing page's `package_name` lists the package (without that key, the folder's name is the package's). The page's `package_name`, `indexed_from`, `exclude_from_docs` and `exclude_kinds` are described under [Frontmatter and permalinks](Authoring#frontmatter-and-permalinks); the indexer reads them and the build does not put them into any page. `sync` copies a package's `api.json` to `indexer/snapshots/` only if the reference documents the package and that run indexed it. +**Which packages are documented.** A section of the reference documents a package when its folder under `docs/Reference/Default/`, `Built-In/` or `Contributed/` has an `index.md`, and the landing page's `package_name` lists the package (without that key, the folder's name is the package's). The page's `package_name`, `indexed_from`, `exclude_from_docs` and `exclude_kinds` are described under [Frontmatter and permalinks](Authoring#frontmatter-and-permalinks); the indexer reads them and the build does not put them into any page. `sync` copies a package's `api.json` to `indexer/snapshots/` only if the reference documents the package and that run indexed it, or the package has no snapshot yet and the store's `api.json` is of the version its section's `indexed_from` names. The second case is a new section: its package was synced before the section existed, so the next `sync` finds the store up to date and indexes nothing. **What `analyze` reports.** By default it covers the documented packages and says how many others it left out. `--package` and `--packages` name any package. A package that no section documents has no pages to compare with, so every public symbol of it is listed as one to create, and so it is for every package under `--all`. A symbol is a type, or a name that a type declares: the Get, Let and Set of a property, and the overloads of a name, are one symbol, as the reference has one page for each. Only Public and Protected members of public types count, less what `exclude_from_docs` and `exclude_kinds` name. Each package is analyzed in one of two modes. diff --git a/indexer/README.md b/indexer/README.md index 9afc3a93..96cb03aa 100644 --- a/indexer/README.md +++ b/indexer/README.md @@ -436,7 +436,10 @@ refuses it): it is listed in `problems` as a line with no field. `indexer/snapshots/` holds the baseline the reference is compared with, so it holds a package only if the reference documents it, and only that package's `api.json`: the sources stay in the store. `sync` copies a snapshot when the -package has a section in the reference, and `analyze` diffs against it. The +package has a section in the reference, and `analyze` diffs against it. A +section started after its package was synced gets its snapshot from the next +`sync`, which copies the store's `api.json` when it is of the version the +section's `indexed_from` names (`adoptSnapshots`). The other packages are indexed into the store and left out of the repository; most of the contributed ones are, and so are the builds of a package that is documented as one. diff --git a/indexer/lib/store.mjs b/indexer/lib/store.mjs index ef869a54..5f233b5b 100644 --- a/indexer/lib/store.mjs +++ b/indexer/lib/store.mjs @@ -118,6 +118,24 @@ async function copyToSnapshots(storePath, snapshotsPath, packages) { return written; } +/** + * Copies to the snapshots the api.json of each documented package that has none + * there, when the store holds the version its section was indexed from: a sync + * run before the section existed brought the store up to date, so no later sync + * indexes the package and copies it. `documented` is `documentedPackages`'s map. + * Returns the packages written, as `copyToSnapshots` does. + */ +async function adoptSnapshots(storePath, snapshotsPath, documented) { + const adopt = []; + for (const [key, section] of documented) { + const [group, pkg] = key.split("/"); + if (await fs.stat(path.join(snapshotsPath, group, pkg, "api.json")).catch(() => null)) continue; + const head = await apiHeadOf(path.join(storePath, group, pkg, "api.json")); + if (head?.version && head.version === section.indexedFrom) adopt.push({ group, package: pkg }); + } + return copyToSnapshots(storePath, snapshotsPath, adopt); +} + /** * What an api.json declares of itself, `{ format, version, project }`, read from * its first bytes (the file is written format, package, project, version, then @@ -309,6 +327,7 @@ async function reindexStored(storePath, group, symbol) { export { STORE_PROBLEMS, + adoptSnapshots, apiHeadOf, commitStore, copyToSnapshots, diff --git a/indexer/twin-index.mjs b/indexer/twin-index.mjs index 55c197c4..96c26150 100644 --- a/indexer/twin-index.mjs +++ b/indexer/twin-index.mjs @@ -20,6 +20,7 @@ import { downloadRelease, getLatestRelease } from "./lib/github-release.mjs"; import { API_FORMAT, buildApi } from "./lib/api-model.mjs"; import { STORE_PROBLEMS, + adoptSnapshots, commitStore, copyToSnapshots, ensureStore, @@ -372,6 +373,11 @@ async function runSync(filter, install) { console.log(` ${synced.length - baseline.length} synced packages have no page in the reference: no snapshot.`); } } + // A section started after its package was synced: the store is current, so + // the run above did not index the package, and its snapshot is the store's. + const adopted = await adoptSnapshots(STORE_PATH, SNAPSHOTS_PATH, await documentedPackages(DOCS_REF_DIR)); + if (adopted.length) + console.log(`\nSnapshots of newly documented packages: ${adopted.map((w) => w.package).join(", ")}.`); for (const name of filter ?? []) { if (known.has(name)) continue; diff --git a/test/indexer.test.mjs b/test/indexer.test.mjs index b5b25102..8f249453 100644 --- a/test/indexer.test.mjs +++ b/test/indexer.test.mjs @@ -48,6 +48,7 @@ import { FINGERPRINTED, fingerprintOf, indexerFingerprint } from "../indexer/lib import { derivePackageApi } from "../indexer/lib/package-api.mjs"; import { readPackageFiles } from "../indexer/lib/package-files.mjs"; import { + adoptSnapshots, apiHeadOf, copyToSnapshots, manifestPath, @@ -1499,6 +1500,30 @@ describe("copyToSnapshots", () => { }); }); +describe("adoptSnapshots", () => { + const store = mkdtempSync(path.join(tmp, "adopt-store-")); + const snapshots = mkdtempSync(path.join(tmp, "adopt-snapshots-")); + putPackage(store, "contributed", "New", { api: apiText("1.0.5.0") }); + putPackage(store, "contributed", "Newer", { api: apiText("2.0.0.0") }); + putPackage(store, "built-in", "Kept", { api: apiText("beta-x-1005") }); + mkdirSync(path.join(snapshots, "built-in", "Kept"), { recursive: true }); + writeFileSync(path.join(snapshots, "built-in", "Kept", "api.json"), apiText("beta-x-0997")); + const documented = new Map([ + ["contributed/New", { indexedFrom: "1.0.5.0" }], + ["contributed/Newer", { indexedFrom: "1.0.0.0" }], + ["contributed/Unsynced", { indexedFrom: "1.0.0.0" }], + ["built-in/Kept", { indexedFrom: "beta-x-1005" }], + ]); + + test("gives a documented package with no snapshot the store's, only at the version its section names", async () => { + assert.deepEqual(await adoptSnapshots(store, snapshots, documented), [{ group: "contributed", package: "New" }]); + assert.equal(readFileSync(path.join(snapshots, "contributed", "New", "api.json"), "utf-8"), apiText("1.0.5.0")); + assert.throws(() => readFileSync(path.join(snapshots, "contributed", "Newer", "api.json")), { code: "ENOENT" }); + // A snapshot there is the baseline, whatever the store holds. + assert.equal(readFileSync(path.join(snapshots, "built-in", "Kept", "api.json"), "utf-8"), apiText("beta-x-0997")); + }); +}); + // ------------------------------------------------------- the reference's package sections const page = (frontmatter, body = "Text.\n") => `---\n${frontmatter}\n---\n\n# Title\n\n${body}`; From 262b8693f51685c8a0585e8e7671a1a15989a66d Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 21:37:55 +0200 Subject: [PATCH 3/6] tbRegExp: a contributed section of 28 pages drafted by the doc-draft workflow and reviewed (VBScript differences in Replace, pattern syntax), with its snapshot, sample template and book chapter --- WIP.Indexer.md | 6 + WIP.md | 2 +- builder/page-baseline.json | 2 +- docs/Reference/Contributed.md | 1 + .../Contributed/tbRegExp/Match/FirstIndex.md | 46 + .../Contributed/tbRegExp/Match/Length.md | 40 + .../Contributed/tbRegExp/Match/SubMatches.md | 48 + .../Contributed/tbRegExp/Match/Value.md | 48 + .../Contributed/tbRegExp/Match/index.md | 38 + .../tbRegExp/MatchCollection/Count.md | 42 + .../tbRegExp/MatchCollection/Item.md | 66 ++ .../tbRegExp/MatchCollection/NewEnum.md | 51 + .../tbRegExp/MatchCollection/index.md | 51 + .../Contributed/tbRegExp/RegExp/DotAll.md | 56 ++ .../Contributed/tbRegExp/RegExp/Execute.md | 68 ++ .../Contributed/tbRegExp/RegExp/Flags.md | 72 ++ .../Contributed/tbRegExp/RegExp/Global.md | 60 ++ .../Contributed/tbRegExp/RegExp/IgnoreCase.md | 56 ++ .../Contributed/tbRegExp/RegExp/IsValid.md | 43 + .../Contributed/tbRegExp/RegExp/LastError.md | 48 + .../tbRegExp/RegExp/LastErrorNumber.md | 49 + .../Contributed/tbRegExp/RegExp/MultiLine.md | 60 ++ .../Contributed/tbRegExp/RegExp/Pattern.md | 56 ++ .../Contributed/tbRegExp/RegExp/Replace.md | 75 ++ .../Contributed/tbRegExp/RegExp/Split.md | 82 ++ .../Contributed/tbRegExp/RegExp/Test.md | 54 ++ .../Contributed/tbRegExp/RegExp/index.md | 76 ++ .../Contributed/tbRegExp/SubMatches/Count.md | 50 + .../Contributed/tbRegExp/SubMatches/Item.md | 73 ++ .../tbRegExp/SubMatches/NewEnum.md | 57 ++ .../Contributed/tbRegExp/SubMatches/index.md | 48 + docs/Reference/Contributed/tbRegExp/index.md | 74 ++ docs/_book.yml | 7 + .../snapshots/contributed/tbRegExp/api.json | 871 ++++++++++++++++++ .../contributed-tbRegExp/Settings | 74 ++ 35 files changed, 2548 insertions(+), 2 deletions(-) create mode 100644 docs/Reference/Contributed/tbRegExp/Match/FirstIndex.md create mode 100644 docs/Reference/Contributed/tbRegExp/Match/Length.md create mode 100644 docs/Reference/Contributed/tbRegExp/Match/SubMatches.md create mode 100644 docs/Reference/Contributed/tbRegExp/Match/Value.md create mode 100644 docs/Reference/Contributed/tbRegExp/Match/index.md create mode 100644 docs/Reference/Contributed/tbRegExp/MatchCollection/Count.md create mode 100644 docs/Reference/Contributed/tbRegExp/MatchCollection/Item.md create mode 100644 docs/Reference/Contributed/tbRegExp/MatchCollection/NewEnum.md create mode 100644 docs/Reference/Contributed/tbRegExp/MatchCollection/index.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/DotAll.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Execute.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Flags.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Global.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/IgnoreCase.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/LastError.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/MultiLine.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Pattern.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Replace.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Split.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/Test.md create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/index.md create mode 100644 docs/Reference/Contributed/tbRegExp/SubMatches/Count.md create mode 100644 docs/Reference/Contributed/tbRegExp/SubMatches/Item.md create mode 100644 docs/Reference/Contributed/tbRegExp/SubMatches/NewEnum.md create mode 100644 docs/Reference/Contributed/tbRegExp/SubMatches/index.md create mode 100644 docs/Reference/Contributed/tbRegExp/index.md create mode 100644 indexer/snapshots/contributed/tbRegExp/api.json create mode 100644 test/example-projects/contributed-tbRegExp/Settings diff --git a/WIP.Indexer.md b/WIP.Indexer.md index a99e56bc..884b1b9f 100644 --- a/WIP.Indexer.md +++ b/WIP.Indexer.md @@ -250,6 +250,12 @@ the old tool's at BETA 997, and it takes half a second with no cache. - **The agents are Sonnet**, as the content agents of the contributed packages were; the drafts are read against the sources before they are committed, which no model choice replaces. +- **The first run, tbRegExp 1.0.5.0 (28 pages, 5 batches), took 7 agents, 7 minutes and + 0.96 M subagent tokens**, and its 30 samples compiled and ran on the first Check. The + review still found what no compile finds: the drafts state only what the code says, so + where the package differs from the object it replaces (VBScript's **RegExp**, run under + `cscript`) nothing said so, and claims read from the code but never run (`$0`, a lone `$`) + needed a probe page. Budget a review that runs the doubts the agents list. - **It is not in the published docs.** `draft` is (Tools-Build), because its plans serve a person writing pages by hand; the workflow is described here and in `WIP.md`, because nothing under `docs/Documentation/` may require Claude. diff --git a/WIP.md b/WIP.md index b8494b06..34cd5958 100644 --- a/WIP.md +++ b/WIP.md @@ -33,7 +33,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Eleven **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_ListBoxHScroll, TBMANLIB_RES and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; OLEGuids documents each interface's methods on the interface's page. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. +Twelve **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, tbRegExp and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, whose one Public Enum the indexer counts as public; OLEGuids documents each interface's methods on the interface's page. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 4a589d5f..1c3e37d9 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1066, + "pages": 1094, "staticFiles": 794 } diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md index 552413c3..37012450 100644 --- a/docs/Reference/Contributed.md +++ b/docs/Reference/Contributed.md @@ -18,6 +18,7 @@ Each package is versioned apart from twinBASIC, and its section says which versi - [**OLEGuids**](OLEGuids/) -- OLE and COM interfaces and structures in twinBASIC source, in place of `OLEGuids.tlb` - [**TBMANLIB_ListBoxHScroll**](TBMANLIB_ListBoxHScroll/) -- a horizontal scroll bar for a **ListBox** - [**TBMANLIB_RES**](TBMANLIB_RES/) -- resources embedded in an executable, as bytes, strings, hex, Base64 or pictures +- [**tbRegExp**](tbRegExp/) -- regular expressions, in place of VBScript's **RegExp** object - [**TwinTimerPackage**](TwinTimerPackage/) -- a Win32 timer class that reports the elapsed time - [**VBA_JSON**](VBA_JSON/) -- JSON parsing and serialization, with UTC and ISO 8601 date conversion - [**VBComDlg**](VBComDlg/) -- the **CommonDialog** class, for the Windows common dialogs diff --git a/docs/Reference/Contributed/tbRegExp/Match/FirstIndex.md b/docs/Reference/Contributed/tbRegExp/Match/FirstIndex.md new file mode 100644 index 00000000..e5e29502 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/Match/FirstIndex.md @@ -0,0 +1,46 @@ +--- +title: FirstIndex +parent: Match +permalink: /tB/Packages/Contributed/tbRegExp/Match/FirstIndex +has_toc: false +--- + +# FirstIndex +{: .no_toc } + +Returns the zero-based position in the source string where the match starts. + +Syntax: *object*.**FirstIndex** + +*object* +: *required* An object expression that evaluates to a **Match**. + +Returns a **Long**. The property is read-only. + +## Remarks + +The first character of the source string is at position 0, so the property counts the characters before the match. The index is zero-based, as in the VBScript regular expression object model that the package replaces. + +The **Mid$** function counts from 1. To cut the match out of the source string with **Mid$**, pass `FirstIndex + 1` as its start position and [**Length**](Length) as its length. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "b+" + +Dim text As String +text = "aabbbc" + +Dim m As Match +Set m = re.Execute(text).Item(0) +Debug.Print m.FirstIndex ' 2 +Debug.Print Mid$(text, m.FirstIndex + 1, m.Length) ' bbb +``` + +## See Also + +- [Length](Length) property -- the number of characters in the match +- [Value](Value) property -- the matched text +- [Execute](../RegExp/Execute) method -- creates the **Match** objects +- [Match](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/Match/Length.md b/docs/Reference/Contributed/tbRegExp/Match/Length.md new file mode 100644 index 00000000..95287fbf --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/Match/Length.md @@ -0,0 +1,40 @@ +--- +title: Length +parent: Match +permalink: /tB/Packages/Contributed/tbRegExp/Match/Length +has_toc: false +--- + +# Length +{: .no_toc } + +Returns the number of characters in the match. + +Syntax: *object*.**Length** + +*object* +: *required* An object expression that evaluates to a **Match**. + +Returns a **Long**. The property is read-only. + +## Remarks + +The value is the length of the matched text that [**Value**](Value) returns. Together with [**FirstIndex**](FirstIndex) it gives the range of the source string that the match covers. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "b+" + +Dim m As Match +Set m = re.Execute("aabbbc").Item(0) +Debug.Print m.Length ' 3 +Debug.Print Len(m.Value) ' 3 +``` + +## See Also + +- [FirstIndex](FirstIndex) property -- the position where the match starts +- [Value](Value) property -- the matched text +- [Match](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/Match/SubMatches.md b/docs/Reference/Contributed/tbRegExp/Match/SubMatches.md new file mode 100644 index 00000000..09ed373a --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/Match/SubMatches.md @@ -0,0 +1,48 @@ +--- +title: SubMatches +parent: Match +permalink: /tB/Packages/Contributed/tbRegExp/Match/SubMatches +has_toc: false +--- + +# SubMatches +{: .no_toc } + +Returns the text captured by the groups of the pattern in this match. + +Syntax: *object*.**SubMatches** + +*object* +: *required* An object expression that evaluates to a **Match**. + +Returns a [**SubMatches**](../SubMatches/) object. The property is read-only. + +## Remarks + +The returned object holds one string for each capture group in the pattern, counted by [**Count**](../SubMatches/Count) and read with [**Item**](../SubMatches/Item) by position or, for a named group, by name. + +When the pattern has no capture groups, the property returns an empty **SubMatches** object with a **Count** of 0. Every match of such a pattern returns the same empty object. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(\d+)-(\d+)" + +Dim m As Match +Set m = re.Execute("pages 10-25").Item(0) +Debug.Print m.Value ' 10-25 +Debug.Print m.SubMatches.Count ' 2 +Debug.Print m.SubMatches.Item(0) ' 10 +Debug.Print m.SubMatches.Item(1) ' 25 + +re.Pattern = "\d+" +Set m = re.Execute("pages 10-25").Item(0) +Debug.Print m.SubMatches.Count ' 0 +``` + +## See Also + +- [Value](Value) property -- the text of the whole match +- [SubMatches](../SubMatches/) class -- the object that the property returns +- [Match](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/Match/Value.md b/docs/Reference/Contributed/tbRegExp/Match/Value.md new file mode 100644 index 00000000..2300b203 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/Match/Value.md @@ -0,0 +1,48 @@ +--- +title: Value +parent: Match +permalink: /tB/Packages/Contributed/tbRegExp/Match/Value +has_toc: false +--- + +# Value +{: .no_toc } + +Returns the text of the whole match. + +Syntax: *object*.**Value** + +*object* +: *required* An object expression that evaluates to a **Match**. + +Returns a **String**. The property is read-only. + +## Remarks + +**Value** is the default member of **Match**, so an expression that needs a **String** can name the object alone. + +The text is the part of the source string that begins at [**FirstIndex**](FirstIndex) and has [**Length**](Length) characters. The text captured by the groups of the pattern is in [**SubMatches**](SubMatches). + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" + +Dim m As Match +Set m = re.Execute("42 apples").Item(0) + +Dim text As String +text = m.Value +Debug.Print text ' apples + +text = m ' the default member is Value +Debug.Print text ' apples +``` + +## See Also + +- [FirstIndex](FirstIndex) property -- the position where the match starts +- [Length](Length) property -- the number of characters in the match +- [SubMatches](SubMatches) property -- the text captured by the groups of the pattern +- [Match](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/Match/index.md b/docs/Reference/Contributed/tbRegExp/Match/index.md new file mode 100644 index 00000000..b62da770 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/Match/index.md @@ -0,0 +1,38 @@ +--- +title: Match +parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/Match/ +has_toc: false +--- + +# Match class +{: .no_toc } + +Holds one match that a **RegExp** found in a string: the matched text, where it starts, how many characters it has, and the text captured by the groups of the pattern. + +[**RegExp.Execute**](../RegExp/Execute) creates one **Match** for each match it finds and returns them in a [**MatchCollection**](../MatchCollection/), in the order found. All four properties are read-only, and the values are set once, when **Execute** creates the **Match**. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" + +Dim m As Match +Set m = re.Execute("42 apples").Item(0) +Debug.Print m.Value ' apples +Debug.Print m.FirstIndex ' 3 +Debug.Print m.Length ' 6 +``` + +## Properties + +- [FirstIndex](FirstIndex) -- returns the zero-based position where the match starts +- [Length](Length) -- returns the number of characters in the match +- [SubMatches](SubMatches) -- returns the text captured by the groups of the pattern +- [Value](Value) -- returns the matched text (default member) + +## See Also + +- [RegExp](../RegExp/) class -- searches a string and creates the **Match** objects +- [MatchCollection](../MatchCollection/) class -- holds the matches that **Execute** finds +- [SubMatches](../SubMatches/) class -- holds the text captured by the groups of one match +- [tbRegExp package](../) -- overview diff --git a/docs/Reference/Contributed/tbRegExp/MatchCollection/Count.md b/docs/Reference/Contributed/tbRegExp/MatchCollection/Count.md new file mode 100644 index 00000000..233e711c --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/MatchCollection/Count.md @@ -0,0 +1,42 @@ +--- +title: Count +parent: MatchCollection +permalink: /tB/Packages/Contributed/tbRegExp/MatchCollection/Count +has_toc: false +--- + +# Count +{: .no_toc } + +Returns the number of [**Match**](../Match/) objects in the collection. + +Syntax: *object*.**Count** + +*object* +: *required* An object expression that evaluates to a **MatchCollection**. + +Returns a **Long**. The property is read-only. + +## Remarks + +For the collection that the **Execute** method of [**RegExp**](../RegExp/) returns, **Count** is the number of matches found in the source string. It is 0 when the pattern finds nothing. When the **Global** property of the expression is **False**, the collection holds the first match only, so **Count** is at most 1. + +The positions that [**Item**](Item) accepts run from 0 to **Count** - 1. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" +re.Global = True +Debug.Print re.Execute("one two three").Count ' 3 +Debug.Print re.Execute("1 2 3").Count ' 0 + +re.Global = False +Debug.Print re.Execute("one two three").Count ' 1 +``` + +## See Also + +- [Item](Item) property -- returns a match by position +- [MatchCollection](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/MatchCollection/Item.md b/docs/Reference/Contributed/tbRegExp/MatchCollection/Item.md new file mode 100644 index 00000000..977240ee --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/MatchCollection/Item.md @@ -0,0 +1,66 @@ +--- +title: Item +parent: MatchCollection +permalink: /tB/Packages/Contributed/tbRegExp/MatchCollection/Item +has_toc: false +--- + +# Item +{: .no_toc } + +Returns the [**Match**](../Match/) at a position in the collection. + +Syntax: *object*.**Item** ( *pItemIndex* ) + +*object* +: *required* An object expression that evaluates to a **MatchCollection**. + +*pItemIndex* +: *required* A **Variant** holding the zero-based position of the match. The first match is 0 and the last is [**Count**](Count) - 1. + +Returns a [**Match**](../Match/). The property is read-only. + +## Remarks + +**Item** is the default member of the class, so `matches(1)` is the same as `matches.Item(1)`. + +The position is zero-based, as in the VBScript object model that the package replaces, although the **Collection** inside the class is one-based. A match has no key, so the argument is always a position. + +A position below 0, or not below [**Count**](Count), raises run-time error `vbObjectError + 9` with the message *Requested item not found in Matches collection*. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("one two three") +Debug.Print matches.Item(0).Value ' one +Debug.Print matches(2).Value ' three +``` + +This example asks for a position that the collection does not have. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("one two three") + +Dim m As Match +On Error Resume Next +Set m = matches.Item(3) +Debug.Print (Err.Number = vbObjectError + 9) ' True +Debug.Print Err.Description ' Requested item not found in Matches collection +``` + +## See Also + +- [Count](Count) property -- the number of matches +- [NewEnum](NewEnum) method -- the enumerator for **For Each** +- [Match](../Match/) class -- one match +- [MatchCollection](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/MatchCollection/NewEnum.md b/docs/Reference/Contributed/tbRegExp/MatchCollection/NewEnum.md new file mode 100644 index 00000000..888fb26e --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/MatchCollection/NewEnum.md @@ -0,0 +1,51 @@ +--- +title: NewEnum +parent: MatchCollection +permalink: /tB/Packages/Contributed/tbRegExp/MatchCollection/NewEnum +has_toc: false +--- + +# NewEnum +{: .no_toc } + +Returns the enumerator that **For Each** uses to go through the [**Match**](../Match/) objects of the collection. + +Syntax: *object*.**NewEnum** **As IUnknown** + +*object* +: *required* An object expression that evaluates to a **MatchCollection**. + +Returns an **IUnknown** that refers to the enumerator of the **Collection** holding the matches. + +## Remarks + +**NewEnum** is declared with the [**Enumerator**](../../../../Core/Attributes#enumerator) attribute, which makes it the member that [**For Each**](../../../../Core/For-Each-Next) calls to start an enumeration. [Custom Enumerators](../../../../../Features/Language/Custom-Enumerators) describes the rule. It is also declared [**Hidden**](../../../../Core/Attributes#hidden), so it does not appear in certain IntelliSense lists. + +The enumerator is the one of the **Collection** inside the class, so each element is a **Match**, and the matches come in the order they were found. The element variable of a **For Each** loop can be declared **As Match**, as in the example. + +A program does not need to call **NewEnum**. **For Each** does it. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "[a-z]+" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("one two three") + +Dim s As String +Dim m As Match +For Each m In matches + s = s & "[" & m.Value & "]" +Next +Debug.Print s ' [one][two][three] +``` + +## See Also + +- [Item](Item) property -- returns a match by position +- [Count](Count) property -- the number of matches +- [For Each...Next](../../../../Core/For-Each-Next) statement -- uses the enumerator +- [MatchCollection](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/MatchCollection/index.md b/docs/Reference/Contributed/tbRegExp/MatchCollection/index.md new file mode 100644 index 00000000..a6e5f8ee --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/MatchCollection/index.md @@ -0,0 +1,51 @@ +--- +title: MatchCollection +parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/MatchCollection/ +has_toc: false +--- + +# MatchCollection class +{: .no_toc } + +Holds the [**Match**](../Match/) objects that a regular expression finds in a string. + +The **Execute** method of [**RegExp**](../RegExp/) returns a **MatchCollection**. When the **Global** property of the expression is **True**, the collection holds every match. Otherwise it holds the first match only. A string in which the pattern finds nothing gives an empty collection. + +The collection is read by position, and the position is zero-based, as in the VBScript object model that the package replaces. [**Item**](Item) is the default member, so `matches(0)` is the same as `matches.Item(0)`. **For Each** goes through the matches in the order they were found. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "\d+" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("a1 b22 c333") +Debug.Print matches.Count ' 3 +Debug.Print matches(1).Value ' 22 + +Dim s As String +Dim m As Match +For Each m In matches + s = s & "[" & m.Value & "]" +Next +Debug.Print s ' [1][22][333] +``` + +The class has no member that adds or removes a match: only **Execute** fills a **MatchCollection**. + +## Properties + +- [Count](Count) -- returns the number of matches +- [Item](Item) -- returns one match by position (the default member) + +## Methods + +- [NewEnum](NewEnum) -- returns the enumerator that **For Each** uses (hidden) + +## See Also + +- [Match](../Match/) class -- one match +- [RegExp](../RegExp/) class -- its **Execute** method returns the collection +- [For Each...Next](../../../../Core/For-Each-Next) statement -- goes through the matches +- [tbRegExp package](../) -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/DotAll.md b/docs/Reference/Contributed/tbRegExp/RegExp/DotAll.md new file mode 100644 index 00000000..7f711041 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/DotAll.md @@ -0,0 +1,56 @@ +--- +title: DotAll +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/DotAll +has_toc: false +--- + +# DotAll +{: .no_toc } + +Returns or sets whether `.` in the pattern matches line terminators. + +## Get + +Returns a **Boolean**. + +Syntax: *object*.**DotAll** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +The default value is **False**. + +## Let + +Sets the value. + +Syntax: *object*.**DotAll** **=** *value* + +*value* +: A **Boolean**. **True** lets `.` match any character, line terminators included. **False** makes `.` match any character except a line terminator. + +## Remarks + +The line terminators are the line feed (`vbLf`), the carriage return (`vbCr`), and the characters U+2028 and U+2029. + +The value is applied when [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) or [**Split**](Split) makes a match. Changing it does not compile the pattern again. The flag `s` of [**Flags**](Flags) reads and sets the same value. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "a.b" +Debug.Print re.Test("a" & vbLf & "b") ' False + +re.DotAll = True +Debug.Print re.Test("a" & vbLf & "b") ' True +``` + +## See Also + +- [Flags](Flags) property -- sets this option with the letter `s` +- [MultiLine](MultiLine) property -- the option for `^` and `$` +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md new file mode 100644 index 00000000..a3ff8c54 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md @@ -0,0 +1,68 @@ +--- +title: Execute +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Execute +has_toc: false +--- + +# Execute +{: .no_toc } + +Searches a string for the pattern and returns the matches. + +Syntax: *object*.**Execute** ( *sourceString* ) **As MatchCollection** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +*sourceString* +: *required* A **String**, the text to search. + +Returns a [**MatchCollection**](../MatchCollection/) that holds one [**Match**](../Match/) for each match of [**Pattern**](Pattern) in *sourceString*, in the order they occur. The collection is empty when there is no match. + +## Remarks + +With [**Global**](Global) set to **False**, the collection holds at most one match, the first. With **Global** set to **True** it holds every match. + +[**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) change what counts as a match. + +Each **Match** gives the matched text ([**Value**](../Match/Value)), the position where it starts ([**FirstIndex**](../Match/FirstIndex), counted from 0), its length, and the text of each group of the pattern ([**SubMatches**](../Match/SubMatches)). A group written `(?...)` can be read by its name. + +An invalid pattern raises the compile error, with the number in [**LastErrorNumber**](LastErrorNumber) and the description in [**LastError**](LastError). Check [**IsValid**](IsValid) first to avoid it. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(?\d{4})-(?\d{2})" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("2026-10 and 2027-01") +Debug.Print matches.Count + +Dim m As Match +For Each m In matches + Debug.Print m.Value & " at " & m.FirstIndex & ": " & m.SubMatches.Item("year") & "/" & m.SubMatches.Item("month") +Next + +re.Global = False +Debug.Print re.Execute("2026-10 and 2027-01").Count +Debug.Print re.Execute("no date").Count +' Output: +' 2 +' 2026-10 at 0: 2026/10 +' 2027-01 at 12: 2027/01 +' 1 +' 0 +``` + +## See Also + +- [Test](Test) method -- returns whether a string contains a match +- [Replace](Replace) method -- replaces the matches +- [Split](Split) method -- splits a string at the matches +- [Global](Global) property -- all matches or the first +- [MatchCollection](../MatchCollection/) class -- the result +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Flags.md b/docs/Reference/Contributed/tbRegExp/RegExp/Flags.md new file mode 100644 index 00000000..616e9c9b --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Flags.md @@ -0,0 +1,72 @@ +--- +title: Flags +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Flags +has_toc: false +--- + +# Flags +{: .no_toc } + +Returns or sets the four matching options as one string of letters. + +## Get + +Returns a **String** that holds the letter of each option that is **True**, always in the order `g`, `m`, `i`, `s`. It is an empty string when all four are **False**. + +Syntax: *object*.**Flags** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +## Let + +Sets all four options at once. + +Syntax: *object*.**Flags** **=** *value* + +*value* +: A **String** of letters. The letters are not case-sensitive. + +| Letter | Option | +|--------|--------| +| `g` | [**Global**](Global) | +| `m` | [**MultiLine**](MultiLine) | +| `i` | [**IgnoreCase**](IgnoreCase) | +| `s` | [**DotAll**](DotAll) | + +## Remarks + +An option whose letter is missing from *value* is set to **False**, so `vbNullString` turns all four off. Other characters in *value* are ignored, and the order and repetition of the letters do not matter. + +The `i` letter is applied through [**IgnoreCase**](IgnoreCase), so the pattern is compiled again the next time it is needed when that option changes. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Flags = "GI" +Debug.Print re.Global ' True +Debug.Print re.IgnoreCase ' True +Debug.Print re.MultiLine ' False +Debug.Print re.Flags ' gi + +re.DotAll = True +Debug.Print re.Flags ' gis + +re.Flags = "m" +Debug.Print re.Flags ' m +Debug.Print re.Global ' False + +re.Flags = vbNullString +Debug.Print Len(re.Flags) ' 0 +``` + +## See Also + +- [Global](Global) property -- the `g` option +- [MultiLine](MultiLine) property -- the `m` option +- [IgnoreCase](IgnoreCase) property -- the `i` option +- [DotAll](DotAll) property -- the `s` option +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Global.md b/docs/Reference/Contributed/tbRegExp/RegExp/Global.md new file mode 100644 index 00000000..4482d911 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Global.md @@ -0,0 +1,60 @@ +--- +title: Global +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Global +has_toc: false +--- + +# Global +{: .no_toc } + +Returns or sets whether all matches are used, or only the first. + +## Get + +Returns a **Boolean**. + +Syntax: *object*.**Global** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +The default value is **False**. + +## Let + +Sets the value. + +Syntax: *object*.**Global** **=** *value* + +*value* +: A **Boolean**. **True** makes [**Execute**](Execute) return every match, [**Replace**](Replace) replace every match, and [**Split**](Split) split at every match. **False** makes each of them use only the first match. + +## Remarks + +[**Test**](Test) does not depend on the value, because it only reports whether a match exists. + +The value is applied when a match is made. Changing it does not compile the pattern again. The flag `g` of [**Flags**](Flags) reads and sets the same value. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "\d+" + +Debug.Print re.Execute("a1b22c333").Count ' 1 +Debug.Print re.Replace("a1b22c333", "#") ' a#b22c333 + +re.Global = True +Debug.Print re.Execute("a1b22c333").Count ' 3 +Debug.Print re.Replace("a1b22c333", "#") ' a#b#c# +``` + +## See Also + +- [Flags](Flags) property -- sets this option with the letter `g` +- [Execute](Execute) method -- returns the matches +- [Replace](Replace) method -- replaces the matches +- [Split](Split) method -- splits a string at the matches +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IgnoreCase.md b/docs/Reference/Contributed/tbRegExp/RegExp/IgnoreCase.md new file mode 100644 index 00000000..9c4cfc47 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/IgnoreCase.md @@ -0,0 +1,56 @@ +--- +title: IgnoreCase +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/IgnoreCase +has_toc: false +--- + +# IgnoreCase +{: .no_toc } + +Returns or sets whether letter case is ignored when the pattern is matched. + +## Get + +Returns a **Boolean**. + +Syntax: *object*.**IgnoreCase** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +The default value is **False**. + +## Let + +Sets the value. + +Syntax: *object*.**IgnoreCase** **=** *value* + +*value* +: A **Boolean**. **True** makes matches case-insensitive. **False** makes them case-sensitive. + +## Remarks + +Unlike [**Global**](Global), [**MultiLine**](MultiLine) and [**DotAll**](DotAll), this option is part of the compiled pattern. Setting it to a different value makes the pattern compile again the next time it is needed, and [**IsValid**](IsValid), [**LastError**](LastError) and [**LastErrorNumber**](LastErrorNumber) then refer to the new compilation. Setting it to its current value does nothing. + +The flag `i` of [**Flags**](Flags) reads and sets the same value. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "hello" +Debug.Print re.Test("Hello World") ' False + +re.IgnoreCase = True +Debug.Print re.Test("Hello World") ' True +``` + +## See Also + +- [Flags](Flags) property -- sets this option with the letter `i` +- [Pattern](Pattern) property -- the regular expression +- [IsValid](IsValid) property -- whether the pattern compiles +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md new file mode 100644 index 00000000..a1c03884 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md @@ -0,0 +1,43 @@ +--- +title: IsValid +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/IsValid +has_toc: false +--- + +# IsValid +{: .no_toc } + +Returns whether the current pattern compiles. Raises no error. + +Syntax: *object*.**IsValid** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +Returns a **Boolean**: **True** when [**Pattern**](Pattern) compiles, **False** when it does not. + +## Remarks + +The property compiles the pattern if it has not been compiled since [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) last changed, and keeps the result. Nothing is raised when the compilation fails. For a pattern that fails, [**LastError**](LastError) holds the message and [**LastErrorNumber**](LastErrorNumber) the number. + +Check **IsValid** before [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) or [**Split**](Split) when the pattern comes from outside the program, because each of those raises the compile error. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(\d+" +Debug.Print re.IsValid ' False + +re.Pattern = "(\d+)" +Debug.Print re.IsValid ' True +``` + +## See Also + +- [LastError](LastError) property -- the message of the compile error +- [LastErrorNumber](LastErrorNumber) property -- the number of the compile error +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md b/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md new file mode 100644 index 00000000..d8c32c4a --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md @@ -0,0 +1,48 @@ +--- +title: LastError +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/LastError +has_toc: false +--- + +# LastError +{: .no_toc } + +Returns the message of the error that compiling the current pattern raised. + +Syntax: *object*.**LastError** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +Returns a **String**: the description of the compile error, or an empty string when the pattern is valid. + +## Remarks + +The property compiles the pattern if it has not been compiled since [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) last changed. The compilation traps its error, so **LastError** raises nothing. The text is the `Description` of the error that the compilation raised; [**LastErrorNumber**](LastErrorNumber) is its number. + +[**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) raise an error with this description when the pattern is invalid. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(\d+)" +Debug.Print Len(re.LastError) ' 0 + +re.Pattern = "(\d+" +Debug.Print re.IsValid ' False +Debug.Print Len(re.LastError) > 0 ' True + +On Error Resume Next +re.Test "123" +Debug.Print Err.Description = re.LastError ' True +``` + +## See Also + +- [LastErrorNumber](LastErrorNumber) property -- the number of the compile error +- [IsValid](IsValid) property -- whether the pattern compiles +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md b/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md new file mode 100644 index 00000000..84c2ffd6 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md @@ -0,0 +1,49 @@ +--- +title: LastErrorNumber +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/LastErrorNumber +has_toc: false +--- + +# LastErrorNumber +{: .no_toc } + +Returns the number of the error that compiling the current pattern raised. + +Syntax: *object*.**LastErrorNumber** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +Returns a **Long**: the number of the compile error, or 0 when the pattern is valid. + +## Remarks + +The property compiles the pattern if it has not been compiled since [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) last changed. The compilation traps its error, so **LastErrorNumber** raises nothing. The value is the `Number` of the error that the compilation raised; [**LastError**](LastError) is its description. + +The engine raises its pattern errors with numbers of the form `vbObjectError + 3000 + n`, where *n* identifies the fault, as in the example. + +[**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) raise an error with this number when the pattern is invalid. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(\d+)" +Debug.Print re.LastErrorNumber ' 0 + +re.Pattern = "(\d+" +Debug.Print re.LastErrorNumber - vbObjectError ' 3009 + +On Error Resume Next +re.Test "123" +Debug.Print Err.Number = re.LastErrorNumber ' True +``` + +## See Also + +- [LastError](LastError) property -- the message of the compile error +- [IsValid](IsValid) property -- whether the pattern compiles +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/MultiLine.md b/docs/Reference/Contributed/tbRegExp/RegExp/MultiLine.md new file mode 100644 index 00000000..4cfe6971 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/MultiLine.md @@ -0,0 +1,60 @@ +--- +title: MultiLine +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/MultiLine +has_toc: false +--- + +# MultiLine +{: .no_toc } + +Returns or sets whether `^` and `$` in the pattern match at the boundaries of each line. + +## Get + +Returns a **Boolean**. + +Syntax: *object*.**MultiLine** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +The default value is **False**. + +## Let + +Sets the value. + +Syntax: *object*.**MultiLine** **=** *value* + +*value* +: A **Boolean**. **True** lets `^` match at the start of the string and after a line terminator, and `$` match at the end of the string and before a line terminator. **False** lets `^` match only at the start of the whole string and `$` only at its end. + +## Remarks + +The line terminators are the line feed (`vbLf`), the carriage return (`vbCr`), and the characters U+2028 and U+2029. + +The value is applied when [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) or [**Split**](Split) makes a match. Changing it does not compile the pattern again. The flag `m` of [**Flags**](Flags) reads and sets the same value. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "^\w+$" +re.Global = True + +Dim text As String +text = "one" & vbLf & "two" +Debug.Print re.Execute(text).Count ' 0 + +re.MultiLine = True +Debug.Print re.Execute(text).Count ' 2 +``` + +## See Also + +- [Flags](Flags) property -- sets this option with the letter `m` +- [DotAll](DotAll) property -- the option for `.` +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Pattern.md b/docs/Reference/Contributed/tbRegExp/RegExp/Pattern.md new file mode 100644 index 00000000..887d412c --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Pattern.md @@ -0,0 +1,56 @@ +--- +title: Pattern +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Pattern +has_toc: false +--- + +# Pattern +{: .no_toc } + +Returns or sets the regular expression. + +## Get + +Returns a **String**. + +Syntax: *object*.**Pattern** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +The default value is an empty string. + +## Let + +Sets the regular expression. + +Syntax: *object*.**Pattern** **=** *value* + +*value* +: A **String** that holds the regular expression. + +## Remarks + +Setting a different pattern makes it compile again the next time it is needed. Setting the current pattern does nothing. A pattern that does not compile raises no error when it is set: [**IsValid**](IsValid), [**LastError**](LastError) and [**LastErrorNumber**](LastErrorNumber) report the failure, and [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) raise it. + +A named group is written `(?...)`. Its text is read from the [**SubMatches**](../SubMatches/) of a [**Match**](../Match/) by name, and used in [**Replace**](Replace) as `$`. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +Debug.Print re.Pattern = vbNullString ' True + +re.Pattern = "b+" +Debug.Print re.Pattern ' b+ +Debug.Print re.Replace("abbbc", "-") ' a-c +``` + +## See Also + +- [IsValid](IsValid) property -- whether the pattern compiles +- [IgnoreCase](IgnoreCase) property -- the option that changes how the pattern is compiled +- [Execute](Execute) method -- returns the matches +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md new file mode 100644 index 00000000..b709e6fe --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md @@ -0,0 +1,75 @@ +--- +title: Replace +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Replace +has_toc: false +--- + +# Replace +{: .no_toc } + +Replaces the matches of the pattern in a string and returns the result. + +Syntax: *object*.**Replace** ( *sourceString*, *replaceString* ) **As String** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +*sourceString* +: *required* A **String**, the text to search. The argument is not changed. + +*replaceString* +: *required* A **String**, the text that replaces each match. It can hold the substitution items in the table below. + +Returns a new **String** with the replacements made. It is *sourceString* unchanged when there is no match. + +## Substitution items + +A `$` in *replaceString* starts a substitution item. Any other text is copied as it is. + +| Item | Is replaced with | +|------|------------------| +| `$$` | A single `$`. | +| `$&` | The matched text. | +| `` $` `` | The part of *sourceString* before the match. | +| `$'` | The part of *sourceString* after the match. | +| `$`*n* | The text of numbered group *n*, counting from 1. All the digits after the `$` are read as the number. It is an empty string when the group matched nothing or the pattern has no such group. `$0` raises a run-time error. | +| `$<`*name*`>` | The text of the group written `(?...)`. It is an empty string when the group matched nothing or no group has the name. | +| `$~` | Nothing. The item is dropped. | + +A `$` at the end of *replaceString*, or followed by a character that is not in the table, raises error `vbObjectError + 3031`. The error is raised even when *sourceString* has no match, so a literal `$` is written `$$`. + +> [!NOTE] +> VBScript's **RegExp** copies into the result, as written, any `$` that does not start one of its own items (`$$`, `$&`, `` $` ``, `$'` and `$`*n*). Here a `$` at the end or before another character raises an error, `$0` raises an error, `$~` is dropped and `$`*n* of a group the pattern does not have is an empty string. A replacement string written for VBScript that holds a literal `$` needs `$$`. + +## Remarks + +With [**Global**](Global) set to **False**, only the first match is replaced. With **Global** set to **True**, every match is. [**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) change what counts as a match. + +An invalid pattern raises the compile error, with the number in [**LastErrorNumber**](LastErrorNumber) and the description in [**LastError**](LastError). Check [**IsValid**](IsValid) first to avoid it. + +## Example + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(\w+)@(\w+)\.com" +Debug.Print re.Replace("ann@example.com, bob@test.com", "$2:$1") ' example:ann, bob@test.com + +re.Global = True +Debug.Print re.Replace("ann@example.com, bob@test.com", "$2:$1") ' example:ann, test:bob + +re.Pattern = "\d+" +Debug.Print re.Replace("5 and 12", "$$$&") ' $5 and $12 + +re.Pattern = "(?\w+) (?\w+)" +Debug.Print re.Replace("Ada Lovelace", "$, $") ' Lovelace, Ada +``` + +## See Also + +- [Execute](Execute) method -- returns the matches +- [Split](Split) method -- splits a string at the matches +- [Global](Global) property -- all matches or the first +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Split.md b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md new file mode 100644 index 00000000..f3d7f367 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md @@ -0,0 +1,82 @@ +--- +title: Split +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Split +has_toc: false +--- + +# Split +{: .no_toc } + +Splits a string at the matches of the pattern and returns the pieces as a **Collection**. + +Syntax: *object*.**Split** ( *sourceString* ) **As Collection** + +*object* +: *required* An object expression that evaluates to a [**RegExp**](./). + +*sourceString* +: *required* A **String**, the text to split. + +Returns a new **Collection** of **String** values, numbered from 1. It holds the text before the first match, the text between each match and the next, and the text after the last match. The matched text itself is not in the collection. + +## Remarks + +[**Global**](Global) sets how many matches split the string. With **Global** set to **False**, the default, only the first match splits it, so the collection holds the text before that match and the text after it. With **Global** set to **True**, every match splits it. + +A match that has capture groups adds the text of each group to the collection, in order, straight after the text before that match. A group that took no part in the match, or matched nothing, adds an empty string. + +When [**Pattern**](Pattern) finds no match, the collection holds one item, the whole of *sourceString*. No piece is dropped: a match at the start of the string gives an empty first string, and two matches next to each other give an empty string between them. + +[**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) apply to the matching. + +When **Pattern** is not a valid regular expression, **Split** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error. + +## Example + +This example splits a list at each comma, first at the first match only, then at every match. + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = ",\s*" + +Dim parts As Collection +Set parts = re.Split("red, green,blue") +Debug.Print parts.Count ' 2 +Debug.Print parts(1) ' red +Debug.Print parts(2) ' green,blue + +re.Global = True +Set parts = re.Split("red, green,blue") +Debug.Print parts.Count ' 3 +Debug.Print parts(1) ' red +Debug.Print parts(2) ' green +Debug.Print parts(3) ' blue +``` + +This example shows the text of a capture group in the result. + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(-)" +re.Global = True + +Dim parts As Collection +Set parts = re.Split("a-b-c") +Debug.Print parts.Count ' 5 +Debug.Print parts(1) ' a +Debug.Print parts(2) ' - +Debug.Print parts(3) ' b +Debug.Print parts(4) ' - +Debug.Print parts(5) ' c +``` + +## See Also + +- [Execute](Execute) method -- returns the matches themselves +- [Replace](Replace) method -- replaces the matches +- [Global](Global) property -- all matches or only the first +- [Pattern](Pattern) property -- the regular expression +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Test.md b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md new file mode 100644 index 00000000..563c882e --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md @@ -0,0 +1,54 @@ +--- +title: Test +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/Test +has_toc: false +--- + +# Test +{: .no_toc } + +Returns whether the pattern finds a match in a string. + +Syntax: *object*.**Test** ( *sourceString* ) **As Boolean** + +*object* +: *required* An object expression that evaluates to a [**RegExp**](./). + +*sourceString* +: *required* A **String**, the text to search. + +Returns **True** if [**Pattern**](Pattern) finds a match in *sourceString*, otherwise **False**. + +## Remarks + +**Test** only reports whether a match exists. To read the matches, use [**Execute**](Execute). + +[**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) apply to the matching. [**Global**](Global) makes no difference, because one match is enough. + +When **Pattern** is not a valid regular expression, **Test** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error. + +## Example + +This example tests two strings against a pattern, then shows the effect of **IgnoreCase**. + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "\d{3}-\d{4}" +Debug.Print re.Test("Call 555-0142 today") ' True +Debug.Print re.Test("Call us today") ' False + +re.Pattern = "^hello" +Debug.Print re.Test("Hello") ' False +re.IgnoreCase = True +Debug.Print re.Test("Hello") ' True +``` + +## See Also + +- [Execute](Execute) method -- returns the matches themselves +- [Replace](Replace) method -- replaces the matches +- [Split](Split) method -- splits a string at the matches +- [IsValid](IsValid) property -- whether the pattern compiles +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/index.md b/docs/Reference/Contributed/tbRegExp/RegExp/index.md new file mode 100644 index 00000000..6db97689 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/index.md @@ -0,0 +1,76 @@ +--- +title: RegExp +parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/ +has_toc: false +--- + +# RegExp class +{: .no_toc } + +Searches strings with a regular expression: finds the matches, tests for one, replaces them and splits a string at them. + +**RegExp** is a replacement for the regular expression object of VBScript. It follows the same object model: [**Execute**](Execute) returns a [**MatchCollection**](../MatchCollection/) of [**Match**](../Match/) objects, and the indexes of both are zero-based. The matching is done by the vba-regex engine, which the package includes as the **StaticRegex** module. + +The class is not predeclared. Create an instance with **New**, set [**Pattern**](Pattern) and the options, then call a method. + +```tb check_build check_run +Dim re As RegExp +Set re = New RegExp +re.Pattern = "(\d+)-(\d+)" +re.Global = True + +Dim matches As MatchCollection +Set matches = re.Execute("10-20 and 30-40") +Debug.Print matches.Count ' 2 +Debug.Print matches.Item(0).Value ' 10-20 +Debug.Print matches.Item(1).SubMatches.Item(0) ' 30 + +Debug.Print re.Test("none") ' False +Debug.Print re.Replace("10-20", "$2-$1") ' 20-10 +``` + +## Options + +Four properties change how the pattern is matched. [**Flags**](Flags) reads and sets all four as one string of letters. + +| Property | Flag | When **True** | +|----------|------|---------------| +| [**Global**](Global) | `g` | All matches are used. When **False**, [**Execute**](Execute), [**Replace**](Replace) and [**Split**](Split) use only the first match. | +| [**MultiLine**](MultiLine) | `m` | `^` and `$` also match at the start and end of each line. | +| [**IgnoreCase**](IgnoreCase) | `i` | Letter case is ignored. | +| [**DotAll**](DotAll) | `s` | `.` also matches line terminators. | + +All four are **False** in a new instance. + +## Invalid patterns + +The pattern is compiled the first time a method or one of [**IsValid**](IsValid), [**LastError**](LastError) and [**LastErrorNumber**](LastErrorNumber) needs it. It is compiled again after [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) is set to a different value. [**Global**](Global), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) are applied when a match is made and do not cause a new compilation. + +A pattern that does not compile does not raise an error at once. [**IsValid**](IsValid) returns **False**, and [**LastError**](LastError) and [**LastErrorNumber**](LastErrorNumber) describe the failure. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise that error, with the number in **LastErrorNumber**, the description in **LastError** and the source `RegExp`. + +## Properties + +- [DotAll](DotAll) -- returns or sets whether `.` matches line terminators +- [Flags](Flags) -- returns or sets the four options as a string of letters +- [Global](Global) -- returns or sets whether all matches are used +- [IgnoreCase](IgnoreCase) -- returns or sets whether letter case is ignored +- [IsValid](IsValid) -- returns whether the pattern compiles +- [LastError](LastError) -- returns the message of the compile error +- [LastErrorNumber](LastErrorNumber) -- returns the number of the compile error +- [MultiLine](MultiLine) -- returns or sets whether `^` and `$` match at line boundaries +- [Pattern](Pattern) -- returns or sets the regular expression + +## Methods + +- [Execute](Execute) -- returns the matches in a string +- [Replace](Replace) -- replaces the matches in a string +- [Split](Split) -- splits a string at the matches +- [Test](Test) -- returns whether a string contains a match + +## See Also + +- [MatchCollection](../MatchCollection/) class -- the matches that **Execute** returns +- [Match](../Match/) class -- one match +- [SubMatches](../SubMatches/) class -- the groups of one match +- [tbRegExp package](../) -- overview diff --git a/docs/Reference/Contributed/tbRegExp/SubMatches/Count.md b/docs/Reference/Contributed/tbRegExp/SubMatches/Count.md new file mode 100644 index 00000000..0186963a --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/SubMatches/Count.md @@ -0,0 +1,50 @@ +--- +title: Count +parent: SubMatches +grand_parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/SubMatches/Count +has_toc: false +--- + +# Count +{: .no_toc } + +Returns the number of strings in the collection, one for each capture group of the pattern. + +Syntax: *object*.**Count** + +*object* +: *required* An object expression that evaluates to a **SubMatches**. + +Returns a **Long**. The property is read-only. + +## Remarks + +**Count** is the number of capture groups in the pattern, not the number of groups that took part in the match. A group that took no part still has its position, and its string is empty. It is 0 for a pattern with no capture groups. + +The positions that [**Item**](Item) accepts run from 0 to **Count** - 1. + +## Example + +```tb check_build check_run +Dim re As New RegExp +Dim matches As MatchCollection +Dim sm As SubMatches + +re.Pattern = "\d+" +Set matches = re.Execute("a1") +Set sm = matches(0).SubMatches +Debug.Print sm.Count ' 0 + +re.Pattern = "(a)|(b)" +Set matches = re.Execute("b") +Set sm = matches(0).SubMatches +Debug.Print sm.Count ' 2 +Debug.Print Len(sm(0)) ' 0 +Debug.Print sm(1) ' b +``` + +## See Also + +- [Item](Item) property -- returns a string by position or by name +- [SubMatches](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/SubMatches/Item.md b/docs/Reference/Contributed/tbRegExp/SubMatches/Item.md new file mode 100644 index 00000000..586e53bb --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/SubMatches/Item.md @@ -0,0 +1,73 @@ +--- +title: Item +parent: SubMatches +grand_parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/SubMatches/Item +has_toc: false +--- + +# Item +{: .no_toc } + +Returns the string captured by one capture group, by its position or by its name. + +Syntax: *object*.**Item** ( *pItemIndex* ) + +*object* +: *required* An object expression that evaluates to a **SubMatches**. + +*pItemIndex* +: *required* A **Variant**. A number is the zero-based position of the group: the first group is 0 and the last is [**Count**](Count) - 1. A value that is not numeric is the name of a group. + +Returns a **String**. The property is read-only. + +## Remarks + +**Item** is the default member of the class, so `sm(0)` is the same as `sm.Item(0)`. + +*pItemIndex* is taken as a position when **IsNumeric** returns **True** for it, and as a name otherwise. + +A position below 0, or not below [**Count**](Count), raises run-time error `vbObjectError + 9` with the message *Requested item not found in SubMatches collection*. + +A name is the one given to a group in the pattern, written `(?...)`. **Item** returns the string of the first group that has that name. Letter case counts: `sm("YEAR")` does not find a group named `year`. A name that no group has returns an empty string and raises no error. + +An empty result has three possible causes: a group that took no part in the match, a group that captured no characters, and a name that no group has. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(?\d{4})-(?\d{2})" + +Dim matches As MatchCollection +Set matches = re.Execute("2026-10") + +Dim sm As SubMatches +Set sm = matches(0).SubMatches +Debug.Print sm(0) ' 2026 +Debug.Print sm.Item("month") ' 10 +Debug.Print Len(sm("day")) ' 0 +``` + +This example asks for a position that the collection does not have. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(\d+)" + +Dim matches As MatchCollection +Set matches = re.Execute("a1") + +Dim sm As SubMatches +Set sm = matches(0).SubMatches + +On Error Resume Next +Debug.Print sm(1) +Debug.Print Err.Number & ": " & Err.Description ' -2147221495: Requested item not found in SubMatches collection +``` + +## See Also + +- [Count](Count) property -- the number of strings +- [NewEnum](NewEnum) method -- the enumerator for **For Each** +- [SubMatches](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/SubMatches/NewEnum.md b/docs/Reference/Contributed/tbRegExp/SubMatches/NewEnum.md new file mode 100644 index 00000000..1506a1b8 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/SubMatches/NewEnum.md @@ -0,0 +1,57 @@ +--- +title: NewEnum +parent: SubMatches +grand_parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/SubMatches/NewEnum +has_toc: false +--- + +# NewEnum +{: .no_toc } + +Returns the enumerator that **For Each** uses to go through the strings of the collection. + +Syntax: *object*.**NewEnum** **As IUnknown** + +*object* +: *required* An object expression that evaluates to a **SubMatches**. + +Returns an **IUnknown** that refers to the enumerator of a **Collection** holding the strings of the capture groups. + +## Remarks + +**NewEnum** is declared with the [**Enumerator**](../../../../Core/Attributes#enumerator) attribute, which makes it the member that [**For Each**](../../../../Core/For-Each-Next) calls to start an enumeration. [Custom Enumerators](../../../../../Features/Language/Custom-Enumerators) describes the rule. It is also declared [**Hidden**](../../../../Core/Attributes#hidden), so it does not appear in certain IntelliSense lists. + +The elements come in the order of their positions, from 0 to [**Count**](Count) - 1, and each is the string of one group. A group that took no part in the match gives an empty string. A **Variant** variable receives each element, as in the example. + +The class builds the **Collection** when the first enumeration asks for it, and keeps it for the later ones. Reading by position or by name with [**Item**](Item) does not build it. + +A program does not need to call **NewEnum**. **For Each** does it. + +## Example + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(\w+)@(\w+)\.com" + +Dim matches As MatchCollection +Set matches = re.Execute("ann@example.com") + +Dim sm As SubMatches +Set sm = matches(0).SubMatches + +Dim part As Variant +For Each part In sm + Debug.Print part +Next +' Output: +' ann +' example +``` + +## See Also + +- [Item](Item) property -- returns a string by position or by name +- [Count](Count) property -- the number of strings +- [For Each...Next](../../../../Core/For-Each-Next) statement -- uses the enumerator +- [SubMatches](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/SubMatches/index.md b/docs/Reference/Contributed/tbRegExp/SubMatches/index.md new file mode 100644 index 00000000..95f29906 --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/SubMatches/index.md @@ -0,0 +1,48 @@ +--- +title: SubMatches +parent: tbRegExp Package +permalink: /tB/Packages/Contributed/tbRegExp/SubMatches/ +has_toc: false +--- + +# SubMatches class +{: .no_toc } + +Holds the strings that the capture groups of a pattern captured in one [**Match**](../Match/). + +The [**SubMatches**](../Match/SubMatches) property of a **Match** returns a **SubMatches** object. It holds one string for each capture group in the pattern, in the order of the groups. A pattern with no capture groups gives an object whose [**Count**](Count) is 0. A group that took no part in the match, such as one side of an alternation that did not match, still has its position, and its string is empty. + +The strings are read by position or by name. The position is zero-based, as in the VBScript object model that the package replaces. A group written `(?...)` has a name, and its string can be read by that name as well. [**Item**](Item) is the default member, so `sm(0)` is the same as `sm.Item(0)`. **For Each** goes through the strings in the order of their positions. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(?\d{4})-(?\d{2})-(?\d{2})" + +Dim matches As MatchCollection +Set matches = re.Execute("Released on 2026-10-11.") + +Dim sm As SubMatches +Set sm = matches(0).SubMatches +Debug.Print sm.Count ' 3 +Debug.Print sm(0) ' 2026 +Debug.Print sm("month") ' 10 +``` + +The class has no member that adds or removes a string: the package fills a **SubMatches** from the groups of the match it belongs to. + +## Properties + +- [Count](Count) -- returns the number of strings +- [Item](Item) -- returns one string by position or by group name (the default member) + +## Methods + +- [NewEnum](NewEnum) -- returns the enumerator that **For Each** uses (hidden) + +## See Also + +- [Match](../Match/) class -- one match; its **SubMatches** property returns this object +- [MatchCollection](../MatchCollection/) class -- the matches of a search +- [RegExp](../RegExp/) class -- runs the search +- [For Each...Next](../../../../Core/For-Each-Next) statement -- goes through the strings +- [tbRegExp package](../) -- overview diff --git a/docs/Reference/Contributed/tbRegExp/index.md b/docs/Reference/Contributed/tbRegExp/index.md new file mode 100644 index 00000000..18fd58ab --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/index.md @@ -0,0 +1,74 @@ +--- +title: tbRegExp Package +parent: Contributed Packages +has_toc: false +exclude_from_docs: + - StaticRegex +indexed_from: 1.0.5.0 +permalink: /tB/Packages/Contributed/tbRegExp/ +--- + +# tbRegExp Package +{: .no_toc } + +**Author:** GCuser99. **On GitHub:** [GCuser99/tbRegExp](https://github.com/GCuser99/tbRegExp), in the folder *Package*. + +The **tbRegExp** contributed package provides regular expressions for twinBASIC projects, as a replacement for the **RegExp** object of VBScript. It follows the same object model. A [**RegExp**](RegExp/) is given a pattern and options, its [**Execute**](RegExp/Execute) method returns a [**MatchCollection**](MatchCollection/) of [**Match**](Match/) objects, and each **Match** holds the text captured by the groups of the pattern as a [**SubMatches**](SubMatches/) object. Positions are zero-based, as in VBScript. + +The matching is done by the [vba-regex](https://github.com/sihlfall/vba-regex) engine of sihlfall, which the package includes as a private module, **StaticRegex**. + +Beyond VBScript's object, **RegExp** has the [**DotAll**](RegExp/DotAll) and [**Flags**](RegExp/Flags) options, the [**Split**](RegExp/Split) method, and [**IsValid**](RegExp/IsValid), [**LastError**](RegExp/LastError) and [**LastErrorNumber**](RegExp/LastErrorNumber), which report an invalid pattern before it is used. [**Replace**](RegExp/Replace) reads a `$` in the replacement string differently from VBScript. + +## Pattern syntax + +A pattern is written as for VBScript's **RegExp**. **tbRegExp** also accepts: + +- named groups, `(?...)`: [**SubMatches.Item**](SubMatches/Item) returns one by its name, and [**Replace**](RegExp/Replace) inserts one as `$`; +- lookbehind, `(?<=...)` and `(?\d{4})-(?\d{2})" +Debug.Print re.Execute("2026-10")(0).SubMatches("month") ' 10 +Debug.Print re.Replace("2026-10", "$/$") ' 10/2026 + +re.Pattern = "(?<=\$)\d+" +Debug.Print re.Execute("costs $123")(0).Value ' 123 + +re.Pattern = "(?i:abc)DEF" +Debug.Print re.Test("ABCDEF") ' True +Debug.Print re.Test("ABCdef") ' False +``` + +A pattern that holds `\k`, `\A`, `\Z`, `\p{...}`, an atomic group `(?>...)` or a conditional is not valid: [**IsValid**](RegExp/IsValid) returns **False**. A numbered backreference such as `\1` works. `\w` matches only the ASCII letters and digits and `_`, as in VBScript, so `\w+` finds `caf` in `café`. + +## Classes + +- [Match](Match/) -- holds one match that a RegExp found in a string: the matched text, where it starts, how many characters it has, and the text captured by its groups + - [FirstIndex](Match/FirstIndex) -- returns the zero-based position where the match starts + - [Length](Match/Length) -- returns the number of characters in the match + - [SubMatches](Match/SubMatches) -- returns the text captured by the groups of the pattern + - [Value](Match/Value) -- returns the matched text (default member) +- [MatchCollection](MatchCollection/) -- holds the Match objects that a regular expression finds in a string + - [Count](MatchCollection/Count) -- returns the number of matches + - [Item](MatchCollection/Item) -- returns one match by position (the default member) + - [NewEnum](MatchCollection/NewEnum) -- returns the enumerator that For Each uses (hidden) +- [RegExp](RegExp/) -- searches strings with a regular expression: finds the matches, tests for one, replaces them and splits a string at them + - [DotAll](RegExp/DotAll) -- returns or sets whether `.` matches line terminators + - [Execute](RegExp/Execute) -- returns the matches in a string + - [Flags](RegExp/Flags) -- returns or sets the four options as a string of letters + - [Global](RegExp/Global) -- returns or sets whether all matches are used + - [IgnoreCase](RegExp/IgnoreCase) -- returns or sets whether letter case is ignored + - [IsValid](RegExp/IsValid) -- returns whether the pattern compiles + - [LastError](RegExp/LastError) -- returns the message of the compile error + - [LastErrorNumber](RegExp/LastErrorNumber) -- returns the number of the compile error + - [MultiLine](RegExp/MultiLine) -- returns or sets whether `^` and `$` match at line boundaries + - [Pattern](RegExp/Pattern) -- returns or sets the regular expression + - [Replace](RegExp/Replace) -- replaces the matches in a string + - [Split](RegExp/Split) -- splits a string at the matches + - [Test](RegExp/Test) -- returns whether a string contains a match +- [SubMatches](SubMatches/) -- holds the strings that the capture groups of a pattern captured in one Match + - [Count](SubMatches/Count) -- returns the number of strings + - [Item](SubMatches/Item) -- returns one string by position or by group name (the default member) + - [NewEnum](SubMatches/NewEnum) -- returns the enumerator that For Each uses (hidden) diff --git a/docs/_book.yml b/docs/_book.yml index 44ebb5bc..259aa89b 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -532,6 +532,13 @@ parts: landing_is_target: true outline_closed: true + - title: tbRegExp Package + subtitle: Regular expressions, in place of VBScript's RegExp object + landing_page: /tB/Packages/Contributed/tbRegExp/ + page: /tB/Packages/Contributed/tbRegExp/ + landing_is_target: true + outline_closed: true + - title: TwinTimerPackage Package subtitle: A Win32 timer class that reports the elapsed time landing_page: /tB/Packages/Contributed/TwinTimerPackage/ diff --git a/indexer/snapshots/contributed/tbRegExp/api.json b/indexer/snapshots/contributed/tbRegExp/api.json new file mode 100644 index 00000000..e0c67cb2 --- /dev/null +++ b/indexer/snapshots/contributed/tbRegExp/api.json @@ -0,0 +1,871 @@ +{ + "format": 3, + "package": "tbRegExp", + "project": "tbRegExp", + "version": "1.0.5.0", + "types": [ + { + "name": "Match", + "kind": "class", + "vis": "public", + "file": "RegExp.twin", + "signature": "Public Class Match", + "attrs": [{"name":"PredeclaredId","args":"False"},{"name":"Hidden","args":"False"}], + "description": "This class represents the results from a single regular expression match.", + "members": [ + {"name":"Class_Terminate","kind":"sub","vis":"private","signature":"Private Sub Class_Terminate()"}, + {"name":"FirstIndex","kind":"property","vis":"public","signature":"Public Property Get FirstIndex() As Long","returnType":"Long","accessor":"Get","description":"Returns the character index position associated with the start of the match string. Note that the index is zero-based."}, + {"name":"Internal_Init","kind":"sub","vis":"friend","signature":"Friend Sub Internal_Init(ByVal matchValue As String, ByVal firstIndex As Long, ByVal length As Long, ByRef subVals() As String, ByRef subKeys() As String, ByVal subCount As Long)"}, + {"name":"Length","kind":"property","vis":"public","signature":"Public Property Get Length() As Long","returnType":"Long","accessor":"Get","description":"Returns the character length of the match string."}, + {"name":"SubMatches","kind":"property","vis":"public","signature":"Public Property Get SubMatches() As SubMatches","returnType":"SubMatches","accessor":"Get","description":"Returns the SubMatches collection of submatch (Group) strings."}, + {"name":"Value","kind":"property","vis":"public","signature":"Public Property Get Value() As String","returnType":"String","accessor":"Get","attrs":[{"name":"DefaultMember"}],"description":"Returns the entire matched string"}, + {"name":"mFirstIndex","kind":"field","vis":"private","signature":"Private mFirstIndex As Long"}, + {"name":"mLength","kind":"field","vis":"private","signature":"Private mLength As Long"}, + {"name":"mValue","kind":"field","vis":"private","signature":"Private mValue As String"}, + {"name":"oSubMatches","kind":"field","vis":"private","signature":"Private oSubMatches As SubMatches"} + ] + }, + { + "name": "MatchCollection", + "kind": "class", + "vis": "public", + "file": "RegExp.twin", + "signature": "Public Class MatchCollection", + "attrs": [{"name":"PredeclaredId","args":"False"},{"name":"Hidden","args":"False"}], + "description": "This class represents a collection of match objects.", + "members": [ + {"name":"Class_Initialize","kind":"sub","vis":"private","signature":"Private Sub Class_Initialize()"}, + {"name":"Class_Terminate","kind":"sub","vis":"private","signature":"Private Sub Class_Terminate()"}, + {"name":"Count","kind":"property","vis":"public","signature":"Public Property Get Count() As Long","returnType":"Long","accessor":"Get","description":"Returns the number of Match objects found in the source string."}, + {"name":"Internal_Add","kind":"sub","vis":"friend","signature":"Friend Sub Internal_Add(obj As Match)"}, + {"name":"Item","kind":"property","vis":"public","signature":"Public Property Get Item(ByVal pItemIndex As Variant) As Match","returnType":"Match","accessor":"Get","attrs":[{"name":"DefaultMember"}],"description":"Returns the Match object associated with the provided index"}, + {"name":"NewEnum","kind":"function","vis":"public","signature":"Public Function NewEnum() As IUnknown","returnType":"IUnknown","hidden":true,"attrs":[{"name":"Hidden","args":"True"},{"name":"Enumerator"}]}, + {"name":"colMatches","kind":"field","vis":"private","signature":"Private colMatches As Collection"} + ] + }, + { + "name": "RegExp", + "kind": "class", + "vis": "public", + "file": "RegExp.twin", + "signature": "Public Class RegExp", + "attrs": [{"name":"PredeclaredId","args":"False"},{"name":"Hidden","args":"False"}], + "description": "This class exposes a drop-in replacement for VBSript's regular expressions object model.", + "members": [ + {"name":"Class_Initialize","kind":"sub","vis":"private","signature":"Private Sub Class_Initialize()"}, + {"name":"DotAll","kind":"property","vis":"public","signature":"Public Property Let DotAll(ByVal pDotAll As Boolean)","accessor":"Let","description":"If 'True', '.' pattern will match across new line charactors. Defaults to 'False'"}, + {"name":"DotAll","kind":"property","vis":"public","signature":"Public Property Get DotAll() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"EnsureCompiled","kind":"sub","vis":"private","signature":"Private Sub EnsureCompiled()"}, + {"name":"Execute","kind":"function","vis":"public","signature":"Public Function Execute(ByVal sourceString As String) As MatchCollection","returnType":"MatchCollection","description":"Returns a MatchCollection. Uses the Pattern property to search the sourceString."}, + {"name":"Flags","kind":"property","vis":"public","signature":"Public Property Let Flags(ByVal pFlags As String)","accessor":"Let","description":"Sets or Gets the options in use: g-global; m-multiline; i-ignore case; s-dot all. Example: 'gm' sets Global and MultiLine to 'True', IgnoreCase and DotAll to 'False'."}, + {"name":"Flags","kind":"property","vis":"public","signature":"Public Property Get Flags() As String","returnType":"String","accessor":"Get"}, + {"name":"GetNamedCapturesInfo","kind":"sub","vis":"private","signature":"Private Sub GetNamedCapturesInfo(ByRef colNames As Collection, ByRef colEntryPoints As Collection)"}, + {"name":"Global","kind":"property","vis":"public","signature":"Public Property Let Global(ByVal pGlobal As Boolean)","accessor":"Let","description":"If 'True', all matches will be returned. If 'False', only the first match will be returned. Defaults to 'False'"}, + {"name":"Global","kind":"property","vis":"public","signature":"Public Property Get Global() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"IgnoreCase","kind":"property","vis":"public","signature":"Public Property Let IgnoreCase(ByVal pIgnoreCase As Boolean)","accessor":"Let","description":"If 'True', matches will be case-insensitive. If 'False', matches will be case-sensitive. Defaults to 'False'"}, + {"name":"IgnoreCase","kind":"property","vis":"public","signature":"Public Property Get IgnoreCase() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"IsValid","kind":"property","vis":"public","signature":"Public Property Get IsValid() As Boolean","returnType":"Boolean","accessor":"Get","description":"Returns True if the current Pattern compiles successfully; False if it is invalid. Does not raise."}, + {"name":"LastError","kind":"property","vis":"public","signature":"Public Property Get LastError() As String","returnType":"String","accessor":"Get","description":"Returns the compile error message for the current Pattern, or an empty string if the Pattern is valid."}, + {"name":"LastErrorNumber","kind":"property","vis":"public","signature":"Public Property Get LastErrorNumber() As Long","returnType":"Long","accessor":"Get","description":"Returns the compile error number for the current Pattern, or 0 if the Pattern is valid."}, + {"name":"MultiLine","kind":"property","vis":"public","signature":"Public Property Let MultiLine(ByVal pMultiLine As Boolean)","accessor":"Let","description":"If 'True', '^' pattern will match the beginning of a line after a new line character and '$' will match the end of a line before a new line character. If 'False' (default), '^' will match the beginning of the entire string and '$' will match the end of the entire string."}, + {"name":"MultiLine","kind":"property","vis":"public","signature":"Public Property Get MultiLine() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"Pattern","kind":"property","vis":"public","signature":"Public Property Let Pattern(ByVal pPattern As String)","accessor":"Let","description":"The regular expression pattern."}, + {"name":"Pattern","kind":"property","vis":"public","signature":"Public Property Get Pattern() As String","returnType":"String","accessor":"Get"}, + {"name":"Replace","kind":"function","vis":"public","signature":"Public Function Replace(ByVal sourceString As String, ByVal replaceString As String) As String","returnType":"String","description":"Replaces matches with replaceString and returns the String with replacements made."}, + {"name":"Split","kind":"function","vis":"public","signature":"Public Function Split(ByVal sourceString As String) As Collection","returnType":"Collection","description":"Splits an input string into an array of substrings at the positions defined by a regular expression match."}, + {"name":"Test","kind":"function","vis":"public","signature":"Public Function Test(ByVal sourceString As String) As Boolean","returnType":"Boolean","description":"Returns 'True' if Pattern finds a match in sourceString, otherwise returns 'False'."}, + {"name":"mCompileAttempted","kind":"field","vis":"private","signature":"Private mCompileAttempted As Boolean"}, + {"name":"mDotAll","kind":"field","vis":"private","signature":"Private mDotAll As Boolean"}, + {"name":"mGlobal","kind":"field","vis":"private","signature":"Private mGlobal As Boolean"}, + {"name":"mIgnoreCase","kind":"field","vis":"private","signature":"Private mIgnoreCase As Boolean"}, + {"name":"mIsValid","kind":"field","vis":"private","signature":"Private mIsValid As Boolean"}, + {"name":"mLastErrNumber","kind":"field","vis":"private","signature":"Private mLastErrNumber As Long"}, + {"name":"mLastError","kind":"field","vis":"private","signature":"Private mLastError As String"}, + {"name":"mMultiLine","kind":"field","vis":"private","signature":"Private mMultiLine As Boolean"}, + {"name":"mPattern","kind":"field","vis":"private","signature":"Private mPattern As String"}, + {"name":"regex","kind":"field","vis":"private","signature":"Private regex As StaticRegex.RegexTy"} + ] + }, + { + "name": "StaticRegex", + "kind": "module", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Module StaticRegex", + "members": [ + {"name":"Advance","kind":"function","vis":"private","signature":"Private Function Advance(ByRef lexCtx As LexerContext) As Long","returnType":"Long"}, + {"name":"AppendEight","kind":"sub","vis":"private","signature":"Private Sub AppendEight(ByRef lab As ArrayBuffer, ByVal v1 As Long, ByVal v2 As Long, ByVal v3 As Long, ByVal v4 As Long, v5 As Long, v6 As Long, v7 As Long, v8 As Long)"}, + {"name":"AppendFill","kind":"sub","vis":"private","signature":"Private Sub AppendFill(ByRef lab As ArrayBuffer, ByVal cnt As Long, ByVal v As Long)"}, + {"name":"AppendFive","kind":"sub","vis":"private","signature":"Private Sub AppendFive(ByRef lab As ArrayBuffer, ByVal v1 As Long, ByVal v2 As Long, ByVal v3 As Long, ByVal v4 As Long, v5 As Long)"}, + {"name":"AppendFormatted","kind":"sub","vis":"private","signature":"Private Sub AppendFormatted( ByRef sb As StaticStringBuilder, ByRef sHaystack As String, ByRef captures As CapturesTy, ByRef formatString As String, ByRef parsed() As Long, Optional ByVal parsedStartPos As Long = 0 )"}, + {"name":"AppendFour","kind":"sub","vis":"private","signature":"Private Sub AppendFour(ByRef lab As ArrayBuffer, ByVal v1 As Long, ByVal v2 As Long, ByVal v3 As Long, ByVal v4 As Long)"}, + {"name":"AppendLong","kind":"sub","vis":"private","signature":"Private Sub AppendLong(ByRef lab As ArrayBuffer, ByVal v As Long)"}, + {"name":"AppendPrefixedPairsArray","kind":"sub","vis":"private","signature":"Private Sub AppendPrefixedPairsArray(ByRef lab As ArrayBuffer, ByVal prefix As Long, ByRef ary() As Long, ByVal aryStart As Long, ByVal aryLength As Long)"}, + {"name":"AppendSlice","kind":"sub","vis":"private","signature":"Private Sub AppendSlice(ByRef lab As ArrayBuffer, ByVal offset As Long, ByVal Length As Long)"}, + {"name":"AppendStr","kind":"sub","vis":"private","signature":"Private Sub AppendStr(ByRef sb As StaticStringBuilder, ByRef s As String)"}, + {"name":"AppendThree","kind":"sub","vis":"private","signature":"Private Sub AppendThree(ByRef lab As ArrayBuffer, ByVal v1 As Long, ByVal v2 As Long, ByVal v3 As Long)"}, + {"name":"AppendTwo","kind":"sub","vis":"private","signature":"Private Sub AppendTwo(ByRef lab As ArrayBuffer, ByVal v1 As Long, ByVal v2 As Long)"}, + {"name":"AppendUnspecified","kind":"sub","vis":"private","signature":"Private Sub AppendUnspecified(ByRef lab As ArrayBuffer, ByVal n As Long)"}, + {"name":"AstTableInitialize","kind":"sub","vis":"private","signature":"Private Sub AstTableInitialize()"}, + {"name":"AstToBytecode","kind":"sub","vis":"private","signature":"Private Sub AstToBytecode(ByRef ast() As Long, ByRef identifierTree As IdentifierTreeTy, ByVal caseInsensitive As Boolean, ByRef bytecode() As Long)"}, + {"name":"Clear","kind":"sub","vis":"private","signature":"Private Sub Clear(ByRef sb As StaticStringBuilder)"}, + {"name":"CloseGroups","kind":"function","vis":"private","signature":"Private Function CloseGroups( ByRef ast As ArrayBuffer, ByRef parseStack As ArrayBuffer, ByRef pendingDisjunction As Long, ByRef currentDisjunction As Long, ByRef potentialConcat2 As Long, ByRef potentialConcat1 As Long, ByRef modifierMask As Long, ByRef spilloverWriteMask As Long, ByVal whatToHandle As Long ) As Long","returnType":"Long"}, + {"name":"Compile","kind":"sub","vis":"private","signature":"Private Sub Compile(ByRef outBytecode() As Long, ByRef s As String, Optional ByVal caseInsensitive As Boolean = False)"}, + {"name":"ComputeFirstChar","kind":"function","vis":"private","signature":"Private Function ComputeFirstChar( ByRef bytecode() As Long, ByRef outChar As Long, ByRef outRangesPc As Long, ByRef outIFlag As Long ) As Long","returnType":"Long"}, + {"name":"CopyCaptures","kind":"sub","vis":"private","signature":"Private Sub CopyCaptures(ByRef context As DfsMatcherContext, ByRef captures As CapturesTy)"}, + {"name":"CopyMemory","kind":"sub","vis":"private","declare":true,"signature":"Private Declare PtrSafe Sub CopyMemory Lib \"kernel32\" Alias \"RtlMoveMemory\" (Destination As Any, Source As Any, ByVal Length As LongPtr)"}, + {"name":"DfsMatch","kind":"function","vis":"private","signature":"Private Function DfsMatch( ByRef outCaptures As CapturesTy, ByRef bytecode() As Long, ByRef inputStr As String, Optional ByVal stepsLimit As Long = DEFAULT_STEPS_LIMIT, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Long","returnType":"Long"}, + {"name":"DfsMatchFrom","kind":"function","vis":"private","signature":"Private Function DfsMatchFrom( ByRef context As DfsMatcherContext, ByRef outCaptures As CapturesTy, ByRef bytecode() As Long, ByRef inputStr As String, ByVal sp As Long, Optional ByVal stepsLimit As Long = DEFAULT_STEPS_LIMIT, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Long","returnType":"Long"}, + {"name":"DfsRunThreads","kind":"function","vis":"private","signature":"Private Function DfsRunThreads( ByRef outCaptures As CapturesTy, ByRef context As DfsMatcherContext, ByRef bytecode() As Long, ByRef inputStr As String, ByVal sp As Long, ByVal stepsLimit As Long, ByVal multiline As Boolean, ByVal dotAll As Boolean ) As Long","returnType":"Long"}, + {"name":"EmitPredefinedRange","kind":"sub","vis":"private","signature":"Private Sub EmitPredefinedRange( ByRef outBuffer As ArrayBuffer, ByRef source() As Long, ByVal sourceStart As Long, ByVal sourceLength As Long )"}, + {"name":"GetBc","kind":"function","vis":"private","signature":"Private Function GetBc(ByRef bytecode() As Long, ByRef pc As Long) As Long","returnType":"Long"}, + {"name":"GetCapture","kind":"function","vis":"public","signature":"Public Function GetCapture(ByRef matcherState As MatcherStateTy, ByRef haystack As String, Optional ByVal num As Long = 0) As String","returnType":"String"}, + {"name":"GetCaptureByName","kind":"function","vis":"public","signature":"Public Function GetCaptureByName( ByRef matcherState As MatcherStateTy, ByRef regex As RegexTy, ByRef haystack As String, ByRef name As String ) As String","returnType":"String"}, + {"name":"GetCapturePoint","kind":"function","vis":"private","signature":"Private Function GetCapturePoint(ByRef context As DfsMatcherContext, ByVal idx As Long) As Long","returnType":"Long"}, + {"name":"GetIdentifierId","kind":"function","vis":"private","signature":"Private Function GetIdentifierId( ByRef bytecode() As Long, ByRef lake As String, ByRef identifier As String ) As Long","returnType":"Long"}, + {"name":"GetInputCharCode","kind":"function","vis":"private","signature":"Private Function GetInputCharCode(ByRef inputStr As String, ByRef sp As Long, ByVal spDelta As Long) As Long","returnType":"Long"}, + {"name":"GetLength","kind":"function","vis":"private","signature":"Private Function GetLength(ByRef sb As StaticStringBuilder) As Long","returnType":"Long"}, + {"name":"GetStr","kind":"function","vis":"private","signature":"Private Function GetStr(ByRef sb As StaticStringBuilder) As String","returnType":"String"}, + {"name":"GetSubstr","kind":"function","vis":"private","signature":"Private Function GetSubstr(ByRef sb As StaticStringBuilder, ByVal start As Long, ByVal Length As Long) As String","returnType":"String"}, + {"name":"HeapsortPairs","kind":"sub","vis":"private","signature":"Private Sub HeapsortPairs(ByRef ary() As Long, ByVal b As Long, ByVal t As Long)"}, + {"name":"HexvalValidate","kind":"function","vis":"private","signature":"Private Function HexvalValidate(ByVal ch As Long) As Long","returnType":"Long"}, + {"name":"InRanges","kind":"function","vis":"private","signature":"Private Function InRanges(ByRef bytecode() As Long, ByVal opPc As Long, ByVal c As Long) As Boolean","returnType":"Boolean"}, + {"name":"IncreaseCapacity","kind":"sub","vis":"private","signature":"Private Sub IncreaseCapacity(ByRef lab As ArrayBuffer, requiredCapacity As Long)"}, + {"name":"Initialize","kind":"sub","vis":"private","signature":"Private Sub Initialize(ByRef lexCtx As LexerContext, ByRef inputStr As String)"}, + {"name":"InitializeAstTable","kind":"sub","vis":"private","signature":"Private Sub InitializeAstTable(ByRef t() As Long)"}, + {"name":"InitializeMatcherContext","kind":"sub","vis":"private","signature":"Private Sub InitializeMatcherContext(ByRef context As DfsMatcherContext, ByVal nProperCapturePoints As Long, ByVal nCapturePoints As Long)"}, + {"name":"InitializeMatcherState","kind":"sub","vis":"public","signature":"Public Sub InitializeMatcherState( ByRef matcherState As MatcherStateTy, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False )"}, + {"name":"InitializeRangeTables","kind":"sub","vis":"private","signature":"Private Sub InitializeRangeTables(ByRef t() As Long)"}, + {"name":"InitializeRegex","kind":"sub","vis":"public","signature":"Public Sub InitializeRegex( ByRef regex As RegexTy, ByRef pattern As String, Optional ByVal caseInsensitive As Boolean = False )"}, + {"name":"InitializeUnicodeCanonLookupTable","kind":"sub","vis":"private","signature":"Private Sub InitializeUnicodeCanonLookupTable(ByRef t() As Long)"}, + {"name":"InitializeUnicodeCanonRunsTable","kind":"sub","vis":"private","signature":"Private Sub InitializeUnicodeCanonRunsTable(ByRef t() As Long)"}, + {"name":"IsIdentifierChar","kind":"function","vis":"private","signature":"Private Function IsIdentifierChar(ByVal c As Long) As Boolean","returnType":"Boolean"}, + {"name":"LEXER_ENDOFINPUT","kind":"const","vis":"private","signature":"Private Const LEXER_ENDOFINPUT As Long = -1"}, + {"name":"LexerParseEscapeU","kind":"function","vis":"private","signature":"Private Function LexerParseEscapeU(ByRef lexCtx As LexerContext) As Long","returnType":"Long"}, + {"name":"LexerParseEscapeX","kind":"function","vis":"private","signature":"Private Function LexerParseEscapeX(ByRef lexCtx As LexerContext) As Long","returnType":"Long"}, + {"name":"LexerParseLegacyOctal","kind":"function","vis":"private","signature":"Private Function LexerParseLegacyOctal(ByRef lexCtx As LexerContext, ByVal x As Long) As Long","returnType":"Long"}, + {"name":"Match","kind":"function","vis":"public","signature":"Public Function Match( ByRef matcherState As MatcherStateTy, ByRef regex As RegexTy, ByRef haystack As String, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False, Optional ByVal matchFrom As Long = 1 ) As Boolean","returnType":"Boolean"}, + {"name":"MatchNext","kind":"function","vis":"public","signature":"Public Function MatchNext( ByRef matcherState As MatcherStateTy, ByRef regex As RegexTy, ByRef haystack As String ) As Boolean","returnType":"Boolean"}, + {"name":"MatchThenJoin","kind":"function","vis":"public","signature":"Public Function MatchThenJoin( ByRef regex As RegexTy, ByRef haystack As String, Optional ByRef format As String = \"$&\", Optional ByRef delimiter As String = vbNullString, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As String","returnType":"String"}, + {"name":"MatchThenList","kind":"sub","vis":"public","signature":"Public Sub MatchThenList( ByRef results() As String, ByRef regex As RegexTy, ByRef haystack As String, ByRef formatStrings() As String, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False )"}, + {"name":"Parse","kind":"sub","vis":"private","signature":"Private Sub Parse(ByRef lex As LexerContext, ByVal caseInsensitive As Boolean, ByRef ast As ArrayBuffer)"}, + {"name":"ParseFormatString","kind":"sub","vis":"private","signature":"Private Sub ParseFormatString(ByRef parsedFormat As ArrayBuffer, ByRef formatString As String, ByRef bytecode() As Long, ByRef pattern As String)"}, + {"name":"ParseReRanges","kind":"sub","vis":"private","signature":"Private Sub ParseReRanges(lexCtx As LexerContext, ByRef outBuffer As ArrayBuffer, ByRef nranges As Long, ByVal ignoreCase As Boolean)"}, + {"name":"ParseReToken","kind":"sub","vis":"private","signature":"Private Sub ParseReToken(ByRef lexCtx As LexerContext, ByRef outToken As ReToken)"}, + {"name":"PeekInputCharCode","kind":"function","vis":"private","signature":"Private Function PeekInputCharCode(ByRef inputStr As String, ByRef sp As Long) As Long","returnType":"Long"}, + {"name":"PerformPotentialConcat","kind":"sub","vis":"private","signature":"Private Sub PerformPotentialConcat( ByRef ast As ArrayBuffer, ByRef potentialConcat2 As Long, ByRef potentialConcat1 As Long )"}, + {"name":"PopMatcherStackFrame","kind":"function","vis":"private","signature":"Private Function PopMatcherStackFrame( ByRef context As DfsMatcherContext, ByRef pc As Long, ByRef sp As Long, ByRef pcLandmark As Long, ByRef spDelta As Long, ByRef q As Long ) As Boolean","returnType":"Boolean"}, + {"name":"PrepareStackAndBytecodeBuffer","kind":"sub","vis":"private","signature":"Private Sub PrepareStackAndBytecodeBuffer(ByRef ast() As Long, ByRef identifierTree As IdentifierTreeTy, ByVal caseInsensitive As Boolean, ByRef stack() As Long, ByRef bytecode() As Long)"}, + {"name":"PushMatcherStackFrame","kind":"sub","vis":"private","signature":"Private Sub PushMatcherStackFrame( ByRef context As DfsMatcherContext, ByVal pc As Long, ByVal sp As Long, ByVal pcLandmark As Long, ByVal spDelta As Long, ByVal q As Long )"}, + {"name":"QStackPop","kind":"function","vis":"private","signature":"Private Function QStackPop(ByRef context As DfsMatcherContext) As Long","returnType":"Long"}, + {"name":"QStackPush","kind":"sub","vis":"private","signature":"Private Sub QStackPush(ByRef context As DfsMatcherContext, ByVal q As Long)"}, + {"name":"RangeTablesInitialize","kind":"sub","vis":"private","signature":"Private Sub RangeTablesInitialize()"}, + {"name":"RangeTablesInitialized","kind":"field","vis":"private","signature":"Private RangeTablesInitialized As Boolean"}, + {"name":"ReCanonicalizeChar","kind":"function","vis":"private","signature":"Private Function ReCanonicalizeChar(ByVal codepoint As Long) As Long","returnType":"Long"}, + {"name":"RedBlackComparator","kind":"function","vis":"private","signature":"Private Function RedBlackComparator(ByRef lake As String, ByRef v1 As StartLengthPair, ByRef v2 As StartLengthPair) As Long","returnType":"Long"}, + {"name":"RedBlackDumpTree","kind":"sub","vis":"private","signature":"Private Sub RedBlackDumpTree(ByRef target() As Long, ByVal targetStartIdx As Long, ByRef tree As IdentifierTreeTy)"}, + {"name":"RedBlackFindOrInsert","kind":"function","vis":"private","signature":"Private Function RedBlackFindOrInsert(ByRef lake As String, ByRef tree As IdentifierTreeTy, ByRef vReference As StartLengthPair) As Long","returnType":"Long"}, + {"name":"RedBlackFindPosition","kind":"function","vis":"private","signature":"Private Function RedBlackFindPosition( ByRef outParent As Long, ByRef outAsRightHandChild As Boolean, ByRef lake As String, ByRef tree As IdentifierTreeTy, ByRef vReference As StartLengthPair ) As Long","returnType":"Long"}, + {"name":"RedBlackInsert","kind":"sub","vis":"private","signature":"Private Sub RedBlackInsert( ByRef tree As IdentifierTreeTy, ByVal newNode As Long, ByVal parent As Long, ByVal asRightHandChild As Boolean )"}, + {"name":"RegexpGenerateRanges","kind":"sub","vis":"private","signature":"Private Sub RegexpGenerateRanges(ByRef outBuffer As ArrayBuffer, ByVal caseInsensitive As Boolean, ByVal r1 As Long, ByVal r2 As Long )"}, + {"name":"ReplaceByRegEx","kind":"function","vis":"public","signature":"Public Function ReplaceByRegEx( ByRef regex As RegexTy, ByRef replacer As String, ByRef haystack As String, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As String","returnType":"String"}, + {"name":"ResetMatcherState","kind":"sub","vis":"public","signature":"Public Sub ResetMatcherState(ByRef matcherState As MatcherStateTy)"}, + {"name":"ReturnToMasterDiscardCaptures","kind":"sub","vis":"private","signature":"Private Sub ReturnToMasterDiscardCaptures( ByRef context As DfsMatcherContext, ByRef pc As Long, ByRef sp As Long, ByRef pcLandmark As Long, ByRef spDelta As Long, ByRef q As Long )"}, + {"name":"ReturnToMasterPreserveCaptures","kind":"sub","vis":"private","signature":"Private Sub ReturnToMasterPreserveCaptures( ByRef context As DfsMatcherContext, ByRef pc As Long, ByRef sp As Long, ByRef pcLandmark As Long, ByRef spDelta As Long, ByRef q As Long )"}, + {"name":"SetCapturePoint","kind":"sub","vis":"private","signature":"Private Sub SetCapturePoint(ByRef context As DfsMatcherContext, ByVal idx As Long, ByVal v As Long)"}, + {"name":"SetInput","kind":"sub","vis":"public","signature":"Public Sub SetInput(ByRef s As String)"}, + {"name":"SetMinimumCapacity","kind":"sub","vis":"private","signature":"Private Sub SetMinimumCapacity(ByRef sb As StaticStringBuilder, ByVal MinimumCapacity As Long)"}, + {"name":"SplitByRegex","kind":"function","vis":"public","signature":"Public Function SplitByRegex( ByRef regex As RegexTy, ByRef haystack As String, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Collection","returnType":"Collection"}, + {"name":"StaticData","kind":"field","vis":"private","signature":"Private StaticData(0 To STATIC_DATA_LENGTH - 1) As Long"}, + {"name":"SwitchToLargerBuffer","kind":"sub","vis":"private","signature":"Private Sub SwitchToLargerBuffer(ByRef sb As StaticStringBuilder, ByVal nRequired As Long)"}, + {"name":"Test","kind":"function","vis":"public","signature":"Public Function Test( ByRef regex As RegexTy, ByRef str As String, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Boolean","returnType":"Boolean"}, + {"name":"TryInitializeRegex","kind":"function","vis":"public","signature":"Public Function TryInitializeRegex( ByRef regex As RegexTy, ByRef pattern As String, Optional ByVal caseInsensitive As Boolean = False ) As Boolean","returnType":"Boolean"}, + {"name":"UnicodeInitialize","kind":"sub","vis":"private","signature":"Private Sub UnicodeInitialize()"}, + {"name":"UnicodeInitialized","kind":"field","vis":"private","signature":"Private UnicodeInitialized As Boolean"}, + {"name":"UnicodeIsLineTerminator","kind":"function","vis":"private","signature":"Private Function UnicodeIsLineTerminator(ByVal codepoint As Long) As Boolean","returnType":"Boolean"}, + {"name":"UnicodeReIsWordchar","kind":"function","vis":"private","signature":"Private Function UnicodeReIsWordchar(c As Long) As Boolean","returnType":"Boolean"}, + {"name":"Unionize","kind":"function","vis":"private","signature":"Private Function Unionize(ByRef ary() As Long, ByVal b As Long, ByVal t As Long) As Long","returnType":"Long"}, + {"name":"astTableInitialized","kind":"field","vis":"private","signature":"Private astTableInitialized As Boolean"}, + {"name":"isCaseInsensitive","kind":"function","vis":"private","signature":"Private Function isCaseInsensitive(ByRef bytecode() As Long) As Boolean","returnType":"Boolean"}, + {"name":"mInputChars","kind":"field","vis":"private","signature":"Private mInputChars() As Integer"}, + {"name":"mInputLen","kind":"field","vis":"private","signature":"Private mInputLen As Long"} + ] + }, + { + "name": "ArrayBuffer", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type ArrayBuffer", + "fields": [ + {"name":"Buffer","vis":"public","signature":"Buffer() As Long"}, + {"name":"Capacity","vis":"public","signature":"Capacity As Long"}, + {"name":"Length","vis":"public","signature":"Length As Long"} + ], + "members": [] + }, + { + "name": "ArrayBufferConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum ArrayBufferConstant", + "values": [ + {"name":"ARRAY_BUFFER_MINIMUM_CAPACITY","value":"16"} + ] + }, + { + "name": "AstNodeDescriptionConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum AstNodeDescriptionConstant", + "values": [ + {"name":"NODE_TYPE","value":"0"}, + {"name":"NODE_LCHILD","value":"1"}, + {"name":"NODE_RCHILD","value":"2"} + ] + }, + { + "name": "AstNodeType", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum AstNodeType", + "values": [ + {"name":"MIN_AST_CODE","value":"0"}, + {"name":"AST_EMPTY","value":"0"}, + {"name":"AST_STRING","value":"1"}, + {"name":"AST_DISJ","value":"2"}, + {"name":"AST_CONCAT","value":"3"}, + {"name":"AST_CHAR","value":"4"}, + {"name":"AST_CAPTURE","value":"5"}, + {"name":"AST_REPEAT_EXACTLY","value":"6"}, + {"name":"AST_PERIOD","value":"7"}, + {"name":"AST_ASSERT_START","value":"8"}, + {"name":"AST_ASSERT_END","value":"9"}, + {"name":"AST_ASSERT_WORD_BOUNDARY","value":"10"}, + {"name":"AST_ASSERT_NOT_WORD_BOUNDARY","value":"11"}, + {"name":"AST_MATCH","value":"12"}, + {"name":"AST_ZEROONE_GREEDY","value":"13"}, + {"name":"AST_ZEROONE_HUMBLE","value":"14"}, + {"name":"AST_STAR_GREEDY","value":"15"}, + {"name":"AST_STAR_HUMBLE","value":"16"}, + {"name":"AST_REPEAT_MAX_GREEDY","value":"17"}, + {"name":"AST_REPEAT_MAX_HUMBLE","value":"18"}, + {"name":"AST_RANGES","value":"19"}, + {"name":"AST_INVRANGES","value":"20"}, + {"name":"AST_ASSERT_POS_LOOKAHEAD","value":"21"}, + {"name":"AST_ASSERT_NEG_LOOKAHEAD","value":"22"}, + {"name":"AST_ASSERT_POS_LOOKBEHIND","value":"23"}, + {"name":"AST_ASSERT_NEG_LOOKBEHIND","value":"24"}, + {"name":"AST_FAIL","value":"25"}, + {"name":"AST_BACKREFERENCE","value":"26"}, + {"name":"AST_NAMED","value":"27"}, + {"name":"AST_MODIFIER_SCOPE","value":"28"}, + {"name":"MAX_AST_CODE","value":"28"} + ] + }, + { + "name": "AstTableDescriptionConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum AstTableDescriptionConstant", + "values": [ + {"name":"AST_TABLE_OFFSET_NC","value":"0"}, + {"name":"AST_TABLE_OFFSET_BLEN","value":"1"}, + {"name":"AST_TABLE_OFFSET_ESFS","value":"2"}, + {"name":"AST_TABLE_ENTRY_LENGTH","value":"3"}, + {"name":"AST_TABLE_LENGTH","value":"AST_TABLE_ENTRY_LENGTH * (MAX_AST_CODE + 1)"} + ] + }, + { + "name": "BytecodeDescriptionConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "public", + "file": "StaticRegex.twin", + "signature": "Public Enum BytecodeDescriptionConstant", + "values": [ + {"name":"BYTECODE_IDX_MAX_PROPER_CAPTURE_SLOT","value":"0"}, + {"name":"BYTECODE_IDX_N_IDENTIFIERS","value":"1"}, + {"name":"BYTECODE_IDX_CASE_INSENSITIVE_INDICATOR","value":"2"}, + {"name":"BYTECODE_IDENTIFIER_MAP_BEGIN","value":"3"}, + {"name":"BYTECODE_IDENTIFIER_MAP_ENTRY_SIZE","value":"3"}, + {"name":"BYTECODE_IDENTIFIER_MAP_ENTRY_START_IN_PATTERN","value":"0"}, + {"name":"BYTECODE_IDENTIFIER_MAP_ENTRY_LENGTH_IN_PATTERN","value":"1"}, + {"name":"BYTECODE_IDENTIFIER_MAP_ENTRY_ID","value":"2"}, + {"name":"RE_QUANTIFIER_INFINITE","value":"&H7FFFFFFF"} + ] + }, + { + "name": "CapturesTy", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type CapturesTy", + "fields": [ + {"name":"nNumberedCaptures","vis":"public","signature":"nNumberedCaptures As Long"}, + {"name":"nNamedCaptures","vis":"public","signature":"nNamedCaptures As Long"}, + {"name":"entireMatch","vis":"public","signature":"entireMatch As StartLengthPair"}, + {"name":"numberedCaptures","vis":"public","signature":"numberedCaptures() As StartLengthPair"}, + {"name":"namedCaptures","vis":"public","signature":"namedCaptures() As Long"} + ], + "members": [] + }, + { + "name": "DfsMatcherContext", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type DfsMatcherContext", + "fields": [ + {"name":"matcherStack","vis":"public","signature":"matcherStack As DfsMatcherStack"}, + {"name":"capturesStack","vis":"public","signature":"capturesStack As ArrayBuffer"}, + {"name":"qstack","vis":"public","signature":"qstack As ArrayBuffer"}, + {"name":"nProperCapturePoints","vis":"public","signature":"nProperCapturePoints As Long"}, + {"name":"nCapturePoints","vis":"public","signature":"nCapturePoints As Long"}, + {"name":"master","vis":"public","signature":"master As Long"}, + {"name":"capturesRequireCoW","vis":"public","signature":"capturesRequireCoW As Boolean"}, + {"name":"qTop","vis":"public","signature":"qTop As Long"} + ], + "members": [] + }, + { + "name": "DfsMatcherPrivateConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum DfsMatcherPrivateConstant", + "values": [ + {"name":"DEFAULT_MINIMUM_THREADSTACK_CAPACITY","value":"16"}, + {"name":"Q_NONE","value":"-2"}, + {"name":"DFS_MATCHER_STACK_MINIMUM_CAPACITY","value":"16"}, + {"name":"DFS_ENDOFINPUT","value":"-1"} + ] + }, + { + "name": "DfsMatcherSharedConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum DfsMatcherSharedConstant", + "values": [ + {"name":"DEFAULT_STEPS_LIMIT","value":"10000"} + ] + }, + { + "name": "DfsMatcherStack", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type DfsMatcherStack", + "fields": [ + {"name":"Buffer","vis":"public","signature":"Buffer() As DfsMatcherStackFrame"}, + {"name":"Capacity","vis":"public","signature":"Capacity As Long"}, + {"name":"Length","vis":"public","signature":"Length As Long"} + ], + "members": [] + }, + { + "name": "DfsMatcherStackFrame", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type DfsMatcherStackFrame", + "fields": [ + {"name":"master","vis":"public","signature":"master As Long"}, + {"name":"capturesStackState","vis":"public","signature":"capturesStackState As Long"}, + {"name":"qStackLength","vis":"public","signature":"qStackLength As Long"}, + {"name":"pc","vis":"public","signature":"pc As Long"}, + {"name":"sp","vis":"public","signature":"sp As Long"}, + {"name":"pcLandmark","vis":"public","signature":"pcLandmark As Long"}, + {"name":"spDelta","vis":"public","signature":"spDelta As Long"}, + {"name":"q","vis":"public","signature":"q As Long"}, + {"name":"qTop","vis":"public","signature":"qTop As Long"} + ], + "members": [] + }, + { + "name": "IdentifierTreeNode", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type IdentifierTreeNode", + "fields": [ + {"name":"rbParent","vis":"public","signature":"rbParent As Long"}, + {"name":"rbChild","vis":"public","signature":"rbChild(0 To 1) As Long"}, + {"name":"rbIsBlack","vis":"public","signature":"rbIsBlack As Boolean"}, + {"name":"reference","vis":"public","signature":"reference As StartLengthPair"}, + {"name":"identifierId","vis":"public","signature":"identifierId As Long"} + ], + "members": [] + }, + { + "name": "IdentifierTreeTy", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type IdentifierTreeTy", + "fields": [ + {"name":"nEntries","vis":"public","signature":"nEntries As Long"}, + {"name":"root","vis":"public","signature":"root As Long"}, + {"name":"bufferCapacity","vis":"public","signature":"bufferCapacity As Long"}, + {"name":"Buffer","vis":"public","signature":"Buffer() As IdentifierTreeNode"} + ], + "members": [] + }, + { + "name": "LexerContext", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type LexerContext", + "fields": [ + {"name":"iCurrent","vis":"public","signature":"iCurrent As Long"}, + {"name":"iEnd","vis":"public","signature":"iEnd As Long"}, + {"name":"inputStr","vis":"public","signature":"inputStr As String"}, + {"name":"currentCharacter","vis":"public","signature":"currentCharacter As Long"}, + {"name":"identifierTree","vis":"public","signature":"identifierTree As IdentifierTreeTy"} + ], + "members": [] + }, + { + "name": "LexerUnicodeCodepointConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum LexerUnicodeCodepointConstant", + "values": [ + {"name":"UNICODE_EXCLAMATION","value":"33"}, + {"name":"UNICODE_DOLLAR","value":"36"}, + {"name":"UNICODE_LPAREN","value":"40"}, + {"name":"UNICODE_RPAREN","value":"41"}, + {"name":"UNICODE_STAR","value":"42"}, + {"name":"UNICODE_PLUS","value":"43"}, + {"name":"UNICODE_COMMA","value":"44"}, + {"name":"UNICODE_MINUS","value":"45"}, + {"name":"UNICODE_PERIOD","value":"46"}, + {"name":"UNICODE_0","value":"48"}, + {"name":"UNICODE_1","value":"49"}, + {"name":"UNICODE_7","value":"55"}, + {"name":"UNICODE_9","value":"57"}, + {"name":"UNICODE_COLON","value":"58"}, + {"name":"UNICODE_LT","value":"60"}, + {"name":"UNICODE_EQUALS","value":"61"}, + {"name":"UNICODE_GT","value":"62"}, + {"name":"UNICODE_QUESTION","value":"63"}, + {"name":"UNICODE_UC_A","value":"65"}, + {"name":"UNICODE_UC_B","value":"66"}, + {"name":"UNICODE_UC_D","value":"68"}, + {"name":"UNICODE_UC_F","value":"70"}, + {"name":"UNICODE_UC_S","value":"83"}, + {"name":"UNICODE_UC_W","value":"87"}, + {"name":"UNICODE_UC_Z","value":"90"}, + {"name":"UNICODE_LBRACKET","value":"91"}, + {"name":"UNICODE_BACKSLASH","value":"92"}, + {"name":"UNICODE_RBRACKET","value":"93"}, + {"name":"UNICODE_CARET","value":"94"}, + {"name":"UNICODE_LC_A","value":"97"}, + {"name":"UNICODE_LC_B","value":"98"}, + {"name":"UNICODE_LC_C","value":"99"}, + {"name":"UNICODE_LC_D","value":"100"}, + {"name":"UNICODE_LC_F","value":"102"}, + {"name":"UNICODE_LC_I","value":"105"}, + {"name":"UNICODE_LC_M","value":"109"}, + {"name":"UNICODE_LC_N","value":"110"}, + {"name":"UNICODE_LC_R","value":"114"}, + {"name":"UNICODE_LC_S","value":"115"}, + {"name":"UNICODE_LC_T","value":"116"}, + {"name":"UNICODE_LC_U","value":"117"}, + {"name":"UNICODE_LC_V","value":"118"}, + {"name":"UNICODE_LC_W","value":"119"}, + {"name":"UNICODE_LC_X","value":"120"}, + {"name":"UNICODE_LC_Z","value":"122"}, + {"name":"UNICODE_LCURLY","value":"123"}, + {"name":"UNICODE_PIPE","value":"124"}, + {"name":"UNICODE_RCURLY","value":"125"}, + {"name":"UNICODE_CP_ZWNJ","value":"&H200C&"}, + {"name":"UNICODE_CP_ZWJ","value":"&H200D&"} + ] + }, + { + "name": "MatcherStateTy", + "kind": "type", + "in": "StaticRegex", + "vis": "public", + "public": false, + "file": "StaticRegex.twin", + "signature": "Public Type MatcherStateTy", + "fields": [ + {"name":"localMatch","vis":"public","signature":"localMatch As Boolean"}, + {"name":"multiline","vis":"public","signature":"multiline As Boolean"}, + {"name":"dotAll","vis":"public","signature":"dotAll As Boolean"}, + {"name":"current","vis":"public","signature":"current As Long"}, + {"name":"captures","vis":"public","signature":"captures As CapturesTy"}, + {"name":"context","vis":"public","signature":"context As DfsMatcherContext"}, + {"name":"inputReady","vis":"public","signature":"inputReady As Boolean"} + ], + "members": [] + }, + { + "name": "ModifierMaskConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum ModifierMaskConstant", + "values": [ + {"name":"MODIFIER_I_WRITE","value":"&H100&"}, + {"name":"MODIFIER_I_ACTIVE","value":"&H200&"}, + {"name":"MODIFIER_I_MASK","value":"MODIFIER_I_ACTIVE Or MODIFIER_I_WRITE"}, + {"name":"MODIFIER_M_WRITE","value":"&H400&"}, + {"name":"MODIFIER_M_ACTIVE","value":"&H800&"}, + {"name":"MODIFIER_M_MASK","value":"MODIFIER_M_ACTIVE Or MODIFIER_M_WRITE"}, + {"name":"MODIFIER_S_WRITE","value":"&H1000&"}, + {"name":"MODIFIER_S_ACTIVE","value":"&H2000&"}, + {"name":"MODIFIER_S_MASK","value":"MODIFIER_S_ACTIVE Or MODIFIER_S_WRITE"}, + {"name":"MODIFIER_WRITE_MASK","value":"&H15555555"}, + {"name":"MODIFIER_ACTIVE_MASK","value":"&H2AAAAAAA"} + ] + }, + { + "name": "NumericConstantLong", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum NumericConstantLong", + "values": [ + {"name":"LONG_FIRST_BIT","value":"&H80000000"}, + {"name":"LONG_ALL_BUT_FIRST_BIT","value":"Not LONG_FIRST_BIT"}, + {"name":"LONG_MIN","value":"&H80000000"}, + {"name":"LONG_MAX","value":"&H7FFFFFFF"}, + {"name":"LONG_MAX_DIV_10","value":"LONG_MAX \\ 10"} + ] + }, + { + "name": "ParseStackFrameTypeConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum ParseStackFrameTypeConstant", + "values": [ + {"name":"PSF_NONCAPTURE","value":"-(MAX_AST_CODE + 1)"}, + {"name":"PSF_MAX_WITH_MODIFIERS","value":"PSF_NONCAPTURE"}, + {"name":"PSF_MIN_EXPLICIT","value":"PSF_NONCAPTURE"}, + {"name":"PSF_MODSCOPE_SPANNING","value":"-(MAX_AST_CODE + 2)"}, + {"name":"PSF_MODSCOPE_LOCAL","value":"-(MAX_AST_CODE + 3)"}, + {"name":"PSF_NONE","value":"LONG_MIN"}, + {"name":"HANDLE_IMPLICIT_LOCAL","value":"PSF_MODSCOPE_LOCAL"}, + {"name":"HANDLE_IMPLICIT","value":"PSF_MODSCOPE_SPANNING"}, + {"name":"HANDLE_UP_TO_EXPLICIT","value":"LONG_MAX"} + ] + }, + { + "name": "ReOpType", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum ReOpType", + "values": [ + {"name":"REOP_OPCODE_MASK","value":"&HFF&"}, + {"name":"REOP_INVALID_OPCODE","value":"0"}, + {"name":"REOP_MATCH","value":"1"}, + {"name":"REOP_CHAR","value":"2"}, + {"name":"REOP_DOT","value":"3"}, + {"name":"REOP_RANGES","value":"4"}, + {"name":"REOP_INVRANGES","value":"5"}, + {"name":"REOP_JUMP","value":"6"}, + {"name":"REOP_SPLIT1","value":"7"}, + {"name":"REOP_SPLIT2","value":"8"}, + {"name":"REOP_SAVE","value":"11"}, + {"name":"REOP_SET_NAMED","value":"12"}, + {"name":"REOP_LOOKPOS","value":"13"}, + {"name":"REOP_LOOKNEG","value":"14"}, + {"name":"REOP_BACKREFERENCE","value":"15"}, + {"name":"REOP_ASSERT_START","value":"16"}, + {"name":"REOP_ASSERT_END","value":"17"}, + {"name":"REOP_ASSERT_WORD_BOUNDARY","value":"18"}, + {"name":"REOP_ASSERT_NOT_WORD_BOUNDARY","value":"19"}, + {"name":"REOP_REPEAT_EXACTLY_INIT","value":"20"}, + {"name":"REOP_REPEAT_EXACTLY_START","value":"21"}, + {"name":"REOP_REPEAT_EXACTLY_END","value":"22"}, + {"name":"REOP_REPEAT_MAX_HUMBLE_INIT","value":"23"}, + {"name":"REOP_REPEAT_MAX_HUMBLE_START","value":"24"}, + {"name":"REOP_REPEAT_MAX_HUMBLE_END","value":"25"}, + {"name":"REOP_REPEAT_GREEDY_MAX_INIT","value":"26"}, + {"name":"REOP_REPEAT_GREEDY_MAX_START","value":"27"}, + {"name":"REOP_REPEAT_GREEDY_MAX_END","value":"28"}, + {"name":"REOP_CHECK_LOOKAHEAD","value":"29"}, + {"name":"REOP_CHECK_LOOKBEHIND","value":"30"}, + {"name":"REOP_END_LOOKPOS","value":"31"}, + {"name":"REOP_END_LOOKNEG","value":"32"}, + {"name":"REOP_FAIL","value":"33"} + ] + }, + { + "name": "ReToken", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type ReToken", + "fields": [ + {"name":"t","vis":"public","signature":"t As Long"}, + {"name":"greedy","vis":"public","signature":"greedy As Boolean"}, + {"name":"num","vis":"public","signature":"num As Long"}, + {"name":"qmin","vis":"public","signature":"qmin As Long"}, + {"name":"qmax","vis":"public","signature":"qmax As Long"} + ], + "members": [] + }, + { + "name": "RegexErrType", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum RegexErrType", + "values": [ + {"name":"REGEX_ERR","value":"vbObjectError + 3000"}, + {"name":"REGEX_ERR_INVALID_REGEXP_ESCAPE","value":"REGEX_ERR + 1"}, + {"name":"REGEX_ERR_INVALID_ESCAPE","value":"REGEX_ERR + 2"}, + {"name":"REGEX_ERR_INVALID_RANGE","value":"REGEX_ERR + 3"}, + {"name":"REGEX_ERR_UNTERMINATED_CHARCLASS","value":"REGEX_ERR + 4"}, + {"name":"REGEX_ERR_INVALID_REGEXP_GROUP","value":"REGEX_ERR + 6"}, + {"name":"REGEX_ERR_INVALID_REGEXP_CHARACTER","value":"REGEX_ERR + 7"}, + {"name":"REGEX_ERR_INVALID_QUANTIFIER","value":"REGEX_ERR + 8"}, + {"name":"REGEX_ERR_UNEXPECTED_END_OF_PATTERN","value":"REGEX_ERR + 9"}, + {"name":"REGEX_ERR_UNEXPECTED_REGEXP_TOKEN","value":"REGEX_ERR + 10"}, + {"name":"REGEX_ERR_UNEXPECTED_CLOSING_PAREN","value":"REGEX_ERR + 11"}, + {"name":"REGEX_ERR_INVALID_QUANTIFIER_NO_ATOM","value":"REGEX_ERR + 12"}, + {"name":"REGEX_ERR_INVALID_IDENTIFIER","value":"REGEX_ERR + 13"}, + {"name":"REGEX_ERR_INVALID_MODIFIER","value":"REGEX_ERR + 14"}, + {"name":"REGEX_ERR_INTERNAL_LOGIC_ERR","value":"REGEX_ERR + 20"}, + {"name":"REGEX_ERR_INVALID_FLAG_ERR","value":"REGEX_ERR + 30"}, + {"name":"REGEX_ERR_INVALID_REPLACEMENT_STRING","value":"REGEX_ERR + 31"}, + {"name":"REGEX_ERR_INVALID_INPUT","value":"REGEX_ERR + 50"} + ] + }, + { + "name": "RegexTy", + "kind": "type", + "in": "StaticRegex", + "vis": "public", + "public": false, + "file": "StaticRegex.twin", + "signature": "Public Type RegexTy", + "fields": [ + {"name":"pattern","vis":"public","signature":"pattern As String"}, + {"name":"bytecode","vis":"public","signature":"bytecode() As Long"}, + {"name":"isCaseInsensitive","vis":"public","signature":"isCaseInsensitive As Long"}, + {"name":"stepsLimit","vis":"public","signature":"stepsLimit As Long"} + ], + "members": [] + }, + { + "name": "ReplType", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum ReplType", + "values": [ + {"name":"REPL_END","value":"0"}, + {"name":"REPL_DOLLAR","value":"1"}, + {"name":"REPL_SUBSTR","value":"2"}, + {"name":"REPL_PREFIX","value":"3"}, + {"name":"REPL_SUFFIX","value":"4"}, + {"name":"REPL_ACTUAL","value":"5"}, + {"name":"REPL_NUMBERED","value":"6"}, + {"name":"REPL_NAMED","value":"7"} + ] + }, + { + "name": "StartLengthPair", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type StartLengthPair", + "fields": [ + {"name":"start","vis":"public","signature":"start As Long"}, + {"name":"Length","vis":"public","signature":"Length As Long"} + ], + "members": [] + }, + { + "name": "StaticDataDescriptionConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum StaticDataDescriptionConstant", + "values": [ + {"name":"AST_TABLE_START","value":"0"}, + {"name":"RANGE_TABLE_DIGIT_START","value":"AST_TABLE_START + AST_TABLE_LENGTH"}, + {"name":"RANGE_TABLE_DIGIT_LENGTH","value":"2"}, + {"name":"RANGE_TABLE_WHITE_START","value":"RANGE_TABLE_DIGIT_START + RANGE_TABLE_DIGIT_LENGTH"}, + {"name":"RANGE_TABLE_WHITE_LENGTH","value":"22"}, + {"name":"RANGE_TABLE_WORDCHAR_START","value":"RANGE_TABLE_WHITE_START + RANGE_TABLE_WHITE_LENGTH"}, + {"name":"RANGE_TABLE_WORDCHAR_LENGTH","value":"8"}, + {"name":"RANGE_TABLE_NOTDIGIT_START","value":"RANGE_TABLE_WORDCHAR_START + RANGE_TABLE_WORDCHAR_LENGTH"}, + {"name":"RANGE_TABLE_NOTDIGIT_LENGTH","value":"4"}, + {"name":"RANGE_TABLE_NOTWHITE_START","value":"RANGE_TABLE_NOTDIGIT_START + RANGE_TABLE_NOTDIGIT_LENGTH"}, + {"name":"RANGE_TABLE_NOTWHITE_LENGTH","value":"24"}, + {"name":"RANGE_TABLE_NOTWORDCHAR_START","value":"RANGE_TABLE_NOTWHITE_START + RANGE_TABLE_NOTWHITE_LENGTH"}, + {"name":"RANGE_TABLE_NOTWORDCHAR_LENGTH","value":"10"}, + {"name":"UNICODE_CANON_LOOKUP_TABLE_START","value":"RANGE_TABLE_NOTWORDCHAR_START + RANGE_TABLE_NOTWORDCHAR_LENGTH"}, + {"name":"UNICODE_CANON_LOOKUP_TABLE_LENGTH","value":"65536"}, + {"name":"UNICODE_CANON_RUNS_TABLE_START","value":"UNICODE_CANON_LOOKUP_TABLE_START + UNICODE_CANON_LOOKUP_TABLE_LENGTH"}, + {"name":"UNICODE_CANON_RUNS_TABLE_LENGTH","value":"303"}, + {"name":"STATIC_DATA_LENGTH","value":"UNICODE_CANON_RUNS_TABLE_START + UNICODE_CANON_RUNS_TABLE_LENGTH"} + ] + }, + { + "name": "StaticStringBuilder", + "kind": "type", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Type StaticStringBuilder", + "fields": [ + {"name":"Active","vis":"public","signature":"Active As Integer"}, + {"name":"Buffer","vis":"public","signature":"Buffer(0 To 1) As String"}, + {"name":"Capacity","vis":"public","signature":"Capacity As Long"}, + {"name":"Length","vis":"public","signature":"Length As Long"}, + {"name":"MinimumCapacity","vis":"public","signature":"MinimumCapacity As Long"} + ], + "members": [] + }, + { + "name": "StaticStringBuilderConstant", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum StaticStringBuilderConstant", + "values": [ + {"name":"STATIC_STRING_BUILDER_DEFAULT_MINIMUM_CAPACITY","value":"16"} + ] + }, + { + "name": "TokenTypeIdType", + "kind": "enum", + "in": "StaticRegex", + "vis": "private", + "public": false, + "file": "StaticRegex.twin", + "signature": "Private Enum TokenTypeIdType", + "values": [ + {"name":"RETOK_EOF","value":"0"}, + {"name":"RETOK_DISJUNCTION","value":"1"}, + {"name":"RETOK_QUANTIFIER","value":"2"}, + {"name":"RETOK_ASSERT_START","value":"3"}, + {"name":"RETOK_ASSERT_END","value":"4"}, + {"name":"RETOK_ASSERT_WORD_BOUNDARY","value":"5"}, + {"name":"RETOK_ASSERT_NOT_WORD_BOUNDARY","value":"6"}, + {"name":"RETOK_ASSERT_START_POS_LOOKAHEAD","value":"7"}, + {"name":"RETOK_ASSERT_START_NEG_LOOKAHEAD","value":"8"}, + {"name":"RETOK_ATOM_PERIOD","value":"9"}, + {"name":"RETOK_ATOM_CHAR","value":"10"}, + {"name":"RETOK_ATOM_DIGIT","value":"11"}, + {"name":"RETOK_ATOM_NOT_DIGIT","value":"12"}, + {"name":"RETOK_ATOM_WHITE","value":"13"}, + {"name":"RETOK_ATOM_NOT_WHITE","value":"14"}, + {"name":"RETOK_ATOM_WORD_CHAR","value":"15"}, + {"name":"RETOK_ATOM_NOT_WORD_CHAR","value":"16"}, + {"name":"RETOK_ATOM_BACKREFERENCE","value":"17"}, + {"name":"RETOK_ATOM_START_CAPTURE_GROUP","value":"18"}, + {"name":"RETOK_ATOM_START_NONCAPTURE_GROUP","value":"19"}, + {"name":"RETOK_ATOM_START_CHARCLASS","value":"20"}, + {"name":"RETOK_ATOM_START_CHARCLASS_INVERTED","value":"21"}, + {"name":"RETOK_ASSERT_START_POS_LOOKBEHIND","value":"22"}, + {"name":"RETOK_ASSERT_START_NEG_LOOKBEHIND","value":"23"}, + {"name":"RETOK_ATOM_END","value":"24"}, + {"name":"RETOK_UNBOUNDED_MODIFIER","value":"25"} + ] + }, + { + "name": "SubMatches", + "kind": "class", + "vis": "public", + "file": "RegExp.twin", + "signature": "Public Class SubMatches", + "attrs": [{"name":"PredeclaredId","args":"False"},{"name":"Hidden","args":"False"}], + "description": "This class provides the means to access SubMatch (Group) values.", + "members": [ + {"name":"Class_Terminate","kind":"sub","vis":"private","signature":"Private Sub Class_Terminate()"}, + {"name":"Count","kind":"property","vis":"public","signature":"Public Property Get Count() As Long","returnType":"Long","accessor":"Get","description":"Returns the number of SubMatch objects found in the source string."}, + {"name":"Internal_SetData","kind":"sub","vis":"friend","signature":"Friend Sub Internal_SetData(ByRef vals() As String, ByRef keys() As String, ByVal count As Long)"}, + {"name":"Item","kind":"property","vis":"public","signature":"Public Property Get Item(ByVal pItemIndex As Variant) As String","returnType":"String","accessor":"Get","attrs":[{"name":"DefaultMember"}],"description":"Returns the SubMatch string associated with the provided index. Named groups are allowed."}, + {"name":"NewEnum","kind":"function","vis":"public","signature":"Public Function NewEnum() As IUnknown","returnType":"IUnknown","hidden":true,"attrs":[{"name":"Hidden","args":"True"},{"name":"Enumerator"}]}, + {"name":"mCap","kind":"field","vis":"private","signature":"Private mCap As Long"}, + {"name":"mCount","kind":"field","vis":"private","signature":"Private mCount As Long"}, + {"name":"mKeys","kind":"field","vis":"private","signature":"Private mKeys() As String"}, + {"name":"mVals","kind":"field","vis":"private","signature":"Private mVals() As String"}, + {"name":"oEnumColl","kind":"field","vis":"private","signature":"Private oEnumColl As Collection"} + ] + }, + { + "name": "modRegexShared", + "kind": "module", + "vis": "private", + "public": false, + "file": "RegExp.twin", + "signature": "Private Module modRegexShared", + "members": [ + {"name":"EmptySubMatches","kind":"function","vis":"public","signature":"Public Function EmptySubMatches() As SubMatches","returnType":"SubMatches"}, + {"name":"mEmpty","kind":"field","vis":"private","signature":"Private mEmpty As SubMatches"} + ] + } + ], + "aliases": [], + "problems": [] +} diff --git a/test/example-projects/contributed-tbRegExp/Settings b/test/example-projects/contributed-tbRegExp/Settings new file mode 100644 index 00000000..749ec7f4 --- /dev/null +++ b/test/example-projects/contributed-tbRegExp/Settings @@ -0,0 +1,74 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Documentation samples", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Template for scripts/check_examples.mjs. project.id, project.name and project.buildPath are rewritten in the staged copy, one per batch.", + "project.exportPathIsV2": true, + "project.id": "{7B247400-0000-4000-9000-7B2474000000}", + "project.name": "DocSamples", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "autoCtlExt": true, + "id": "{E1B23FED-12D0-4166-B1E0-3BE49A6B9C26}", + "licence": "MIT", + "name": "[IMPORTED] Regular Expressions Object - Replacement for VBScript.RegExp", + "path32": "/Packages/tbRegExp", + "path64": "/Packages/tbRegExp", + "symbolId": "tbRegExp", + "versionBuild": "0", + "versionMajor": "1", + "versionMinor": "0", + "versionRevision": "5" + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} From c16c68427bccad67642ceab88b02241be38643ce Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 21:41:43 +0200 Subject: [PATCH 4/6] doc-draft: the Check agent runs the doubts the Draft and Lists agents list, on a probe page and on the object a package replaces; the Lists agent reads the package's README and writes its On GitHub line --- WIP.Indexer.md | 12 ++++++++++-- indexer/draft/workflow.mjs | 36 ++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/WIP.Indexer.md b/WIP.Indexer.md index 884b1b9f..766cc0f5 100644 --- a/WIP.Indexer.md +++ b/WIP.Indexer.md @@ -246,7 +246,14 @@ the old tool's at BETA 997, and it takes half a second with no cache. - **The check step compiles before a person reads.** One agent per package runs `check_examples --only` on the section, at a port of its own (9700 up, clear of the harness's default 9480), and fixes the pages. A contributed package without a - `contributed-` template cannot be compiled, and the agent says so. + `contributed-` template cannot be compiled, and the agent says so. **It also runs + the doubts the other agents return**: the workflow passes their results into its prompt, + and it settles each one a run can on a scratch page (`zz-probe.md`, every line expecting + `' ?`, so the report prints what each line printed) and, for a package that replaces + another object, on that object too (`cscript` for VBScript, `vb6run.mjs` for VB6). The + Draft agents only list doubts: they run concurrently, so none of them compiles. The Lists + agent reads a new package's README through read-only `gh` and lists the README's claims + as doubts as well. - **The agents are Sonnet**, as the content agents of the contributed packages were; the drafts are read against the sources before they are committed, which no model choice replaces. @@ -255,7 +262,8 @@ the old tool's at BETA 997, and it takes half a second with no cache. review still found what no compile finds: the drafts state only what the code says, so where the package differs from the object it replaces (VBScript's **RegExp**, run under `cscript`) nothing said so, and claims read from the code but never run (`$0`, a lone `$`) - needed a probe page. Budget a review that runs the doubts the agents list. + needed a probe page. Running the doubts is now the Check agent's (above); the review + still reads the pages against the sources. - **It is not in the published docs.** `draft` is (Tools-Build), because its plans serve a person writing pages by hand; the workflow is described here and in `WIP.md`, because nothing under `docs/Documentation/` may require Claude. diff --git a/indexer/draft/workflow.mjs b/indexer/draft/workflow.mjs index 1c7231aa..e3763011 100644 --- a/indexer/draft/workflow.mjs +++ b/indexer/draft/workflow.mjs @@ -18,8 +18,10 @@ // For each package, one agent writes each batch of pages, all at once; then one // agent adds the new pages to the lists of the landing page and of the pages // of their types, and writes a new section's landing page; then one compiles -// the section's samples with check_examples and fixes what the compiler -// refuses. Nothing here commits, builds or runs a gate: the drafts are drafts. +// the section's samples with check_examples, fixes what the compiler refuses, +// and runs the doubts the other agents listed: the claims no sample shows, and +// where the package may differ from the object it replaces. Nothing here +// commits, builds or runs a gate: the drafts are drafts. // WIP.Indexer.md says why the work is split this way. export const meta = { @@ -29,7 +31,7 @@ export const meta = { phases: [ { title: 'Draft', detail: 'one agent per batch of pages', model: 'sonnet' }, { title: 'Lists', detail: 'one agent per package: landing page and member lists', model: 'sonnet' }, - { title: 'Check', detail: 'one agent per package: compile the samples and fix them', model: 'sonnet' }, + { title: 'Check', detail: 'one agent per package: compile the samples, run the doubts, fix the pages', model: 'sonnet' }, ], } @@ -74,7 +76,7 @@ const results = await pipeline( lists, })), (done, pkg, i) => - agent(checkPrompt(pkg, CHECK_PORT_BASE + i * CHECK_PORT_STEP), { + agent(checkPrompt(pkg, CHECK_PORT_BASE + i * CHECK_PORT_STEP, [...done.drafted, done.lists].filter(Boolean).join('\n\n')), { label: `check:${pkg.package}`, phase: 'Check', model: 'sonnet', @@ -115,9 +117,10 @@ Then, for each task: Rules: - State only what the source declares and its code does. Where the source does not make a behaviour clear, document the declaration and say nothing more: never invent semantics. +- List as a doubt each claim a page makes that no sample's stated output shows, and, when the sources say the package replaces another object or library (VBScript's RegExp, a VB6 control), each behaviour that may differ from it. A later step runs them. - Every \`tb\` fence carries \`check_build\`. A statement sample whose output the page states also carries \`check_run\`, with the output as a trailing comment on each \`Debug.Print\` line; \`MsgBox\`, \`InputBox\` and \`End\` cannot be run. - Do not edit the landing page, or a type's page that lists its members, except to add an \`inline\` section: a later step updates the lists. -- Write files with the Write and Edit tools, never through the shell. No git commands; do not build. +- Write files with the Write and Edit tools, never through the shell. No git commands; do not build, and do not run check_examples: other agents are drafting beside you, and the later step compiles every page. Return one line for each task, \`: written\`, \`changed\` or \`skipped ()\`, then one line for each doubt about what the source means.` } @@ -130,9 +133,9 @@ function listsPrompt(pkg) { (landing.permalink ? `, \`permalink: ${landing.permalink}\`` : ', a permalink of the form the landing pages beside it use') + `, \`has_toc: false\` and \`indexed_from: ${pkg.version}\`; ` + (landing.author - ? `under the H1, the line \`**Author:** ${landing.author}\`, or the author the package's sources name if they name someone else (WIP.md, the contributed packages); ` + ? `under the H1, the line \`**Author:** ${landing.author}\`, or the author the package's sources name if they name someone else, followed by \`**On GitHub:** [owner/repo](url)\` when the sources name a repository that holds the package (WIP.md, the contributed packages); ` : '') + - `a short description of the package from its sources (the \`source\` paths in the plan); then the lists, with no \`* TOC\` list when the page has only one or two sections. Model it on a landing page of another section in the same folder.\n\n` + `a short description of the package from its sources (the \`source\` paths in the plan) and, when the sources name its GitHub repository, its README (\`gh api repos///readme --jq .content\` gives it in base64; use gh only to read): what the package adds over the object it replaces and what it does not support, where the README says so; then the lists, with no \`* TOC\` list when the page has only one or two sections. Model it on a landing page of another section in the same folder. List as a doubt each README claim the page states that no sample on it shows.\n\n` : '' return `Bring the lists of the ${pkg.package} section up to date with the pages just drafted for it. @@ -143,20 +146,29 @@ ${newLanding}1. Add each new page (\`new: true\`) to the landing page's lists, u 3. Set \`indexed_from:\` in the landing page's frontmatter to \`${pkg.version}\`. 4. Change nothing else. Write files with the Write and Edit tools, never through the shell; no git commands; do not build. -Return one line for each file changed, saying what was added.` +Return one line for each file changed, saying what was added, then one line for each doubt.` } -function checkPrompt(pkg, port) { +function checkPrompt(pkg, port, doubts) { const only = pkg.sectionDir.replace(/^docs\//, '') - return `Compile the samples of the ${pkg.package} section and fix the drafted pages the compiler refuses. + const probe = `${pkg.sectionDir}/zz-probe.md` + return `Compile the samples of the ${pkg.package} section, settle the doubts the drafting agents listed, and fix the drafted pages. Run, from the repository root: node scripts/check_examples.mjs --only "^${only}/" --port ${port} --jobs 2 It needs a twinBASIC install. For a contributed package it also needs a sample template, test/example-projects/contributed-${pkg.package}/Settings: if the run says the harness or the template is missing, stop and say so. -For each finding, fix the page it names: the sample, or what the page says it prints. Fix the page, never the harness or a template, and only pages listed in ${pkg.plan}. A \`check_run\` mismatch prints what the code really printed: that is the fact to document. Run again after fixing, three runs at most. +For each finding, fix the page it names: the sample, or what the page says it prints. Fix the page, never the harness or a template, and only the pages listed in ${pkg.plan} and its landing page. A \`check_run\` mismatch prints what the code really printed: that is the fact to document. Run again after fixing, three runs at most. + +Then settle each doubt below that a run can settle: +- Write a scratch page, ${probe}, with frontmatter \`title: zz probe\` and \`nav_exclude: true\` and one \`tb check_build check_run\` sample that prints each fact in doubt, every \`Debug.Print\` line ending in the expected output \`' ?\`, and run it with \`--only "^${only}/zz-probe"\` at the same port: the report shows what each line printed. Under \`On Error Resume Next\`, a \`Debug.Print\` whose expression raises an error prints nothing, so assign the value first and print \`Err.Number\` after it. +- Where the sources say the package replaces another object, run the same input on that object when this machine can: VBScript's under \`cscript //nologo .vbs\`, printing with \`WScript.Echo\`; VB6 code with \`node scripts/vb6run.mjs\`. Write those files outside the repository, and never call \`MsgBox\` or \`InputBox\`. Where the two differ, the page says what the replaced object does, in a \`> [!NOTE]\`. +- Correct the pages to what the runs printed, delete ${probe}, and run the section once more. + +Doubts: +${doubts || '(none listed)'} Write files with the Write and Edit tools, never through the shell; no git commands; do not build. -Return the last run's summary line, then one line for each fix: \`:: \`.` +Return the last run's summary line, then one line for each fix: \`:: \`, then one line for each doubt: settled, with what the run printed, or left, with why.` } From 0892a1e5ebd2c06d17104261d2d895db7cd9e8a7 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 22:05:54 +0200 Subject: [PATCH 5/6] tbProjPacker: a contributed section of 5 pages (ProjPacker's Pack, Unpack and Test), with its snapshot, sample template and book chapter; its private modules' enums are excluded, as no project can name them --- WIP.md | 2 +- builder/page-baseline.json | 2 +- docs/Reference/Contributed.md | 1 + .../tbProjPacker/ProjPacker/Pack.md | 101 ++++ .../tbProjPacker/ProjPacker/Test.md | 94 ++++ .../tbProjPacker/ProjPacker/Unpack.md | 120 ++++ .../tbProjPacker/ProjPacker/index.md | 63 +++ .../Contributed/tbProjPacker/index.md | 72 +++ docs/_book.yml | 7 + .../contributed/tbProjPacker/api.json | 513 ++++++++++++++++++ .../contributed-tbProjPacker/Settings | 74 +++ 11 files changed, 1047 insertions(+), 2 deletions(-) create mode 100644 docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md create mode 100644 docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md create mode 100644 docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md create mode 100644 docs/Reference/Contributed/tbProjPacker/ProjPacker/index.md create mode 100644 docs/Reference/Contributed/tbProjPacker/index.md create mode 100644 indexer/snapshots/contributed/tbProjPacker/api.json create mode 100644 test/example-projects/contributed-tbProjPacker/Settings diff --git a/WIP.md b/WIP.md index 34cd5958..ee48b85f 100644 --- a/WIP.md +++ b/WIP.md @@ -33,7 +33,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Twelve **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, tbRegExp and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, whose one Public Enum the indexer counts as public; OLEGuids documents each interface's methods on the interface's page. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. +Thirteen **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, tbProjPacker, tbRegExp and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modPackUnpack` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 1c3e37d9..70ef24cd 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1094, + "pages": 1099, "staticFiles": 794 } diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md index 37012450..35aefcb0 100644 --- a/docs/Reference/Contributed.md +++ b/docs/Reference/Contributed.md @@ -18,6 +18,7 @@ Each package is versioned apart from twinBASIC, and its section says which versi - [**OLEGuids**](OLEGuids/) -- OLE and COM interfaces and structures in twinBASIC source, in place of `OLEGuids.tlb` - [**TBMANLIB_ListBoxHScroll**](TBMANLIB_ListBoxHScroll/) -- a horizontal scroll bar for a **ListBox** - [**TBMANLIB_RES**](TBMANLIB_RES/) -- resources embedded in an executable, as bytes, strings, hex, Base64 or pictures +- [**tbProjPacker**](tbProjPacker/) -- a `.twinproj` or `.twinpack` file unpacked into a folder of source files, and packed again - [**tbRegExp**](tbRegExp/) -- regular expressions, in place of VBScript's **RegExp** object - [**TwinTimerPackage**](TwinTimerPackage/) -- a Win32 timer class that reports the elapsed time - [**VBA_JSON**](VBA_JSON/) -- JSON parsing and serialization, with UTC and ISO 8601 date conversion diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md new file mode 100644 index 00000000..284bb5db --- /dev/null +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md @@ -0,0 +1,101 @@ +--- +title: Pack +parent: ProjPacker +permalink: /tB/Packages/Contributed/tbProjPacker/ProjPacker/Pack +has_toc: false +--- + +# Pack +{: .no_toc } + +Packs a folder, and everything in it, into a project file. + +Syntax: *object*.**Pack** ( *inputDir*, *outputFile* [, *overwrite* [, *logFile* ]] ) **As Boolean** + +*object* +: *required* An object expression that evaluates to a **ProjPacker**. + +*inputDir* +: *required* A **String**, the folder to pack. It must exist, and it must hold a file named `Settings`. + +*outputFile* +: *required* A **String**, the project file to write. The name is used as given: **Pack** adds no extension and checks none. + +*overwrite* +: *optional* A **Boolean**. **True** replaces *outputFile* when it exists. The default is **False**, and the call then fails when anything exists at that path. + +*logFile* +: *optional* A **String**, the path of a [log file](.#the-log-file) for the lines the call prints. The default is an empty string, which writes no log. + +Returns **True** when *outputFile* was written, and **False** when it was not. The method raises no error; [Results and errors](.#results-and-errors) describes how the reason is reported. + +## What is packed + +The root of the project file is named after the last folder of *inputDir*. Everything in *inputDir* is stored, in all its subfolders: hidden files, empty folders and a `.git` folder included. Keep the project's files in a folder of their own. The top folder of a Git repository would put the repository's own data into the project file. + +Version 1.0.5.0 stores each file exactly as it is on disk. It does not convert line endings. The IDE stores `.twin`, `.bas` and `.cls` files with CR LF line endings, so a checkout that has changed them to LF is packed with LF. + +Every project that the IDE exports has the folders `Resources`, `Sources`, `ImportedTypeLibraries`, `Miscellaneous` and `Packages`, even when they are empty. Git does not store an empty folder, so a folder checked out from a repository can lack some of them. **Pack** does not add the missing folders. It prints two lines after the summary line, and still returns **True**: + +``` + note: expected folders not present on disk: Resources, Sources, ImportedTypeLibraries, Miscellaneous, Packages. + twinBASIC should rebuild them on Open Project. +``` + +## Refusals + +**Pack** returns **False** and writes nothing in these cases: + +- *inputDir* is not a folder. The line is `Pack failed: Not a directory: ` and the path. +- *outputFile* exists and *overwrite* is **False**. The call makes this check before it reads the folder. +- *inputDir* has no `Settings` file directly in it. The file holds the project's name, references, version and compile options, and a project file packed without it would open without them. A folder named `Settings` does not count, and the file name must be spelled exactly `Settings`. The line begins `Pack failed: Refusing to pack: no 'Settings' file at the top level of`. + +The line for the second case reads `Output already exists: ` and the path, then `Refusing to overwrite it. Use the overwrite option (CLI: --force) to replace it.` **Pack** has no `--force`: the option the message means is the *overwrite* argument. + +## Replacing a project file + +With *overwrite* **True**, **Pack** deletes the existing file just before it writes the new one, after the folder has been read. Do not put *outputFile* inside *inputDir*. A project file left there by an earlier call is packed into the new one, and the file grows with each call. + +## Output + +On success **Pack** prints one summary line: + +``` +Exported "MyApp-src" -> C:\Temp\MyApp.twinproj (170594 bytes, 18 files, 6 directories) +``` + +The name is the root's name, the last folder of *inputDir*. The byte count is the size of the new file, and the file and folder counts do not include the root. + +> [!NOTE] +> The summary line of **Pack** begins `Exported`, and that of [**Unpack**](Unpack) begins `Imported`. The Import/export tool names the two directions the other way round: its `import` command packs a folder and its `export` command unpacks a project. + +## Example + +This example packs a folder into a project file in the TEMP folder and replaces the file if it exists. + +```tb check_build +Dim pp As New ProjPacker +Dim src As String, outFile As String + +src = Environ$("TEMP") & "\MyApp-src" +outFile = Environ$("TEMP") & "\MyApp.twinproj" + +If pp.Pack(src, outFile, True) Then + Debug.Print "Packed " & outFile +Else + Debug.Print "Nothing was packed" +End If +``` + +Without *overwrite*, a second call with the same *outFile* fails and prints: + +``` +Pack failed: Output already exists: "C:\Temp\MyApp.twinproj". Refusing to overwrite it. Use the overwrite option (CLI: --force) to replace it. +``` + +## See Also + +- [Unpack](Unpack) method -- unpacks a project file into a folder +- [Test](Test) method -- runs the package's tests +- [ProjPacker](./) class -- overview, the log file and the paths +- [Import/export tool](../../../../../Features/Packages/Import-Export-Tool) -- the command-line programs that do the same diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md new file mode 100644 index 00000000..848320f1 --- /dev/null +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md @@ -0,0 +1,94 @@ +--- +title: Test +parent: ProjPacker +permalink: /tB/Packages/Contributed/tbProjPacker/ProjPacker/Test +has_toc: false +--- + +# Test +{: .no_toc } + +Runs the package's own tests and returns whether they all passed. + +Syntax: *object*.**Test** ( *samplePath* [, *logFile* ] ) **As Boolean** + +*object* +: *required* An object expression that evaluates to a **ProjPacker**. + +*samplePath* +: *required* A **String**, the path of a `.twinproj` or `.twinpack` file for the four tests that work on a real project. An empty string skips those four tests. A path to a file that does not exist makes the call fail. + +*logFile* +: *optional* A **String**, the path of a [log file](.#the-log-file) for the lines the call prints. The default is an empty string, which writes no log. + +Returns **True** when no test failed, and **False** when a test failed or *samplePath* names a file that does not exist. A skipped test is not a failure. The method raises no error; [Results and errors](.#results-and-errors) describes how the reason is reported. The sample file is never changed. + +## What the tests check + +There are 23 tests. Each runs on its own, so a failure in one does not stop the others. The tests work in a new folder, named `impexp-test-` and a number, in the folder that the `TEMP` environment variable names. The folder is deleted when the tests end. + +Four tests need the sample file: + +- Parsing the sample, then writing the tree and parsing that copy, gives the same project name, file count and folder count. +- A round trip in memory keeps every path and the content of every file. +- Writing, parsing and writing again gives the same bytes both times. +- Unpacking the sample, packing the folder and unpacking that file gives two folders with the same files and the same bytes. + +Nineteen tests need no sample: + +- **File format (6).** An empty project, a project with one file, the flags of the entries, Unicode names with their sort order, a file with a wrong magic number, and a file cut short. +- **Long path (1).** A file whose path is longer than 260 characters survives a pack and an unpack. +- **Pack (4).** [**Pack**](Pack) refuses a folder with no `Settings` file and accepts one with it. It refuses to replace an existing file, and replaces it when *overwrite* is **True**. +- **Unpack with *cleanFirst* (6).** [**Unpack**](Unpack) removes leftover files, counts the levels of a path correctly, refuses a folder that has no `Settings` file and a folder whose `Settings` is a folder, and accepts a project folder and an empty folder. +- **Other refusals (2).** **Pack** refuses a folder whose `Settings` is a folder. **Unpack** reports a project file that does not exist. + +## Output + +**Test** prints a heading, a line for each test and a summary. A test line is `[PASS]`, `[FAIL]` or `[SKIP]` and the test's name. A failed test is followed by a line with the reason. The first test also prints an `[INFO]` line with the project's name, its number of files and folders, and its size. The summary counts the passed tests against the passed and failed ones; skipped tests are not counted. + +This is the output with a sample file, with the paths shortened: + +``` +Self-test workdir: C:\...\impexp-test-78561008 +Sample: C:\...\imagestrip.twinproj + + [INFO] root "imagestrip", 18 files, 6 directories, 170595 bytes + [PASS] Parse sample (invariants + re-parse consistency) + [PASS] In-memory round-trip (parse -> serialize -> re-parse) + ... 21 more lines, one for each of the other tests ... + +23/23 tests passed. +``` + +Without a sample, the four sample tests print `[SKIP]` and the summary ends with the number skipped: + +``` +19/19 tests passed (4 skipped -- pass a .twinproj or .twinpack path to run them). +``` + +A *samplePath* that does not exist prints `Sample not found: ` and the path. No test runs. + +> [!NOTE] +> An empty *samplePath* does not make the method fail. With 19 of the 19 tests that run passed and 4 skipped, it returns **True**. Pass a project file to run all 23. + +## Example + +This example runs all the tests against a project file, and writes the lines to a log. + +```tb check_build +Dim pp As New ProjPacker +Dim ok As Boolean + +ok = pp.Test(App.Path & "\MyApp.twinproj", Environ$("TEMP") & "\selftest.log") +If ok Then + Debug.Print "All tests passed" +Else + Debug.Print "A test failed; see selftest.log" +End If +``` + +## See Also + +- [Pack](Pack) method -- packs a folder into a project file +- [Unpack](Unpack) method -- unpacks a project file into a folder +- [ProjPacker](./) class -- overview, the log file and the paths diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md new file mode 100644 index 00000000..34f32100 --- /dev/null +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md @@ -0,0 +1,120 @@ +--- +title: Unpack +parent: ProjPacker +permalink: /tB/Packages/Contributed/tbProjPacker/ProjPacker/Unpack +has_toc: false +--- + +# Unpack +{: .no_toc } + +Unpacks a project file into a folder of source files. + +Syntax: *object*.**Unpack** ( *inputFile*, *outputDir* [, *cleanFirst* [, *logFile* ]] ) **As Boolean** + +*object* +: *required* An object expression that evaluates to a **ProjPacker**. + +*inputFile* +: *required* A **String**, the `.twinproj` or `.twinpack` file to unpack. It must exist and must be a file. The extension is not checked. + +*outputDir* +: *required* A **String**, the folder to write into. It is created, with any missing parent folders, when it does not exist. When it is an empty string, the folder is named after the project and is created in the current directory. + +*cleanFirst* +: *optional* A **Boolean**. **True** empties *outputDir* before anything is written, if the checks under [Emptying the folder first](#emptying-the-folder-first) allow it. The default is **False**. + +*logFile* +: *optional* A **String**, the path of a [log file](.#the-log-file) for the lines the call prints. The default is an empty string, which writes no log. + +Returns **True** when every file was written, and **False** when it was not. The method raises no error; [Results and errors](.#results-and-errors) describes how the reason is reported. The project file is never changed. + +## What is written + +The files and folders of the project go directly into *outputDir*: its contents are the project's top-level entries, such as `Settings`, `Sources` and `Resources`. The project's name does not become a folder. It appears only in the summary line. + +With *cleanFirst* **False**, a file already in *outputDir* is replaced by the file of the same name from the project, without asking. Files and folders that the project does not hold stay where they are. A later [**Pack**](Pack) of the folder packs them too. To get exactly the project's files, unpack into an empty folder or use *cleanFirst*. + +The project file is read and checked before anything is deleted or written. If the file is missing, is a folder, or is not a project file, nothing in *outputDir* changes. A failure while the files are being written, a full disk for example, leaves the files written so far. + +## Emptying the folder first + +*cleanFirst* deletes files and folders, so **Unpack** makes four checks on *outputDir*, in this order, and deletes nothing unless it passes them: + +1. *outputDir* does not exist, or is not a folder. There is nothing to empty, and the unpack goes on. +2. *outputDir* is empty. The unpack goes on, with no further check. This allows the usual sequence of making an empty folder and then unpacking into it with *cleanFirst*. +3. *outputDir* is a drive root or a folder directly under one, such as `C:\` or `C:\Projects`. The call is refused. The folder must be at least two levels below the root, as `C:\Projects\MyApp` is. For a network path, the levels are counted below the share, so `\\server\share\MyApp` is refused and `\\server\share\Projects\MyApp` is accepted. +4. *outputDir* is not empty and has no file named `Settings` directly in it. The call is refused, because the folder does not look like an unpacked project. A folder named `Settings` does not count. + +A refusal returns **False** and leaves the folder as it was. + +> [!WARNING] +> A folder that passes the checks is deleted with everything in it, including files that are not part of any project. Point *cleanFirst* only at a folder that holds nothing but an earlier unpack. + +## Output + +On success **Unpack** prints one summary line: + +``` +Imported "MyApp" -> C:\Temp\MyApp-src\ (18 files, 6 directories) +``` + +The name is the project's name as stored in the file. The counts do not include *outputDir*. + +When the call fails, it prints `Unpack failed: ` and one of these reasons: + +| Reason | Cause | +|--------|-------| +| `Input file not found: ` and the path | *inputFile* does not exist. | +| `Input is a directory, not a file: ` and the path | *inputFile* is a folder. | +| `Bad magic: ` and a number | The file is not a project file. | +| `Unsupported file format version: ` and a number | The file is a project file of a format version that the package does not read. | +| `Truncated file: ` and the details | The file ends before its contents do. | +| `Refusing to clean a shallow path: ` and the path | *cleanFirst* is **True** and *outputDir* fails the third check. | +| `Refusing to clean ` and the path in quotes, then `it is not empty and has no top-level 'Settings' file` | *cleanFirst* is **True** and *outputDir* fails the fourth check. | +| `Cannot create directory: ` and the path | *outputDir* or a folder in it could not be created. | + +> [!NOTE] +> The summary line of **Unpack** begins `Imported`, and that of [**Pack**](Pack) begins `Exported`. The Import/export tool names the two directions the other way round: its `export` command unpacks a project and its `import` command packs a folder. + +## Example + +This example unpacks a project into a folder in the TEMP folder, emptying the folder first if an earlier unpack left it there. + +```tb check_build +Dim pp As New ProjPacker +Dim src As String, work As String + +src = App.Path & "\MyApp.twinproj" +work = Environ$("TEMP") & "\MyApp-src" + +If pp.Unpack(src, work, True) Then + Debug.Print "Unpacked into " & work +Else + Debug.Print "Nothing was unpacked" +End If +``` + +The next example writes a log, and reads it back when the unpack fails. A file that is not a project file gives `Unpack failed: Bad magic`, and a missing one `Unpack failed: Input file not found`. + +```tb check_build +Dim pp As New ProjPacker +Dim logPath As String +logPath = Environ$("TEMP") & "\unpack.log" + +If Not pp.Unpack(App.Path & "\notes.txt", Environ$("TEMP") & "\notes-src", False, logPath) Then + Dim f As Integer, text As String + f = FreeFile + Open logPath For Input As #f + Line Input #f, text + Close #f + Debug.Print text +End If +``` + +## See Also + +- [Pack](Pack) method -- packs a folder into a project file +- [Test](Test) method -- runs the package's tests +- [ProjPacker](./) class -- overview, the log file and the paths +- [Import/export tool](../../../../../Features/Packages/Import-Export-Tool) -- the command-line programs that do the same diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/index.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/index.md new file mode 100644 index 00000000..afead918 --- /dev/null +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/index.md @@ -0,0 +1,63 @@ +--- +title: ProjPacker +parent: tbProjPacker Package +permalink: /tB/Packages/Contributed/tbProjPacker/ProjPacker/ +has_toc: false +--- + +# ProjPacker class +{: .no_toc } + +Unpacks a twinBASIC project file into a folder of source files, packs a folder into a project file, and runs the package's own tests. + +The class has three methods and no properties or events. It holds no data, so one instance serves any number of calls. The class is not predeclared: create an instance with **New**, then call a method. + +```tb check_build +Dim pp As New ProjPacker +Dim logPath As String +logPath = Environ$("TEMP") & "\projpacker.log" + +If Not pp.Unpack(inputFile:=App.Path & "\MyApp.twinproj", outputDir:=Environ$("TEMP") & "\MyApp-src", logFile:=logPath) Then + ' The log holds the reason. + Dim f As Integer, text As String + f = FreeFile + Open logPath For Input As #f + Do Until EOF(f) + Line Input #f, text + Debug.Print text + Loop + Close #f +End If +``` + +## Results and errors + +Each method returns a **Boolean**: **True** when it did its work, **False** when it did not. No method raises a run-time error. A file that does not exist, a folder that cannot be written, a damaged project file, a log file that cannot be opened and each refusal described on the method pages all give **False**. A caller that only needs to know whether the call worked tests the return value. A caller that needs the reason reads the lines the call printed. + +**Pack** and **Unpack** print a summary line when they succeed. When they fail they print one line, `Pack failed: ` or `Unpack failed: ` and the reason. [**Test**](Test) prints a line for each test. + +The lines are printed with **Debug.Print** when the program runs in the IDE (when [**App.IsInIDE**](../../../AppGlobalClassObject/_App/IsInIDE) is **True**), so they appear in the [Debug Console](../../../../IDE/Project/DebugConsole). A compiled program prints nothing. Pass *logFile* when the program needs the lines outside the IDE. + +## The log file + +Every method has an optional *logFile* argument, the path of a text file for the lines the call prints. With the default, an empty string, no log is written. + +- The file is created, or emptied if it already exists, when the call starts. It is never added to, so a log holds the lines of the last call only. +- The file is UTF-8 with CR LF line endings and no byte order mark, so a non-ASCII character, such as one in a folder name, is written correctly. +- A log that cannot be created, for example because its folder does not exist, makes the call return **False** at once, before it does anything else. The reason, `Cannot open log file for writing`, is printed but cannot be logged. +- The log holds the failure line too, so the reason for a **False** can be read from it afterwards. + +## Paths + +Every path argument can be absolute or relative, and can use `\` or `/` as the separator. A relative path starts from the current directory of the program, which is not necessarily the folder that holds the program. Paths longer than 260 characters are accepted. + +## Methods + +- [Pack](Pack) -- packs a folder into a project file +- [Test](Test) -- runs the package's tests +- [Unpack](Unpack) -- unpacks a project file into a folder + +## See Also + +- [tbProjPacker package](../) -- overview +- [Import/export tool](../../../../../Features/Packages/Import-Export-Tool) -- the command-line programs that do the same diff --git a/docs/Reference/Contributed/tbProjPacker/index.md b/docs/Reference/Contributed/tbProjPacker/index.md new file mode 100644 index 00000000..f8fac08b --- /dev/null +++ b/docs/Reference/Contributed/tbProjPacker/index.md @@ -0,0 +1,72 @@ +--- +title: tbProjPacker Package +parent: Contributed Packages +has_toc: false +exclude_from_docs: + - modPackUnpack + - modWin32 +indexed_from: 1.0.5.0 +permalink: /tB/Packages/Contributed/tbProjPacker/ +--- + +# tbProjPacker Package +{: .no_toc } + +**Author:** GCuser99. **On GitHub:** [GCuser99/tbProjPacker](https://github.com/GCuser99/tbProjPacker), in the folder *Project*. + +The **tbProjPacker** contributed package unpacks a twinBASIC project file, a `.twinproj` or `.twinpack` file, into a folder of source files, and packs such a folder into a project file again. It does from twinBASIC code what the [Import/export tool](../../../../Features/Packages/Import-Export-Tool) does from a command line: [**Unpack**](ProjPacker/Unpack) corresponds to the tool's `export` command, and [**Pack**](ProjPacker/Pack) to its `import` command. + +A project file is one binary file, which version control can neither compare nor merge. The folder it unpacks to is made of text files, which it can. A build routine can also unpack a copy of a project, change the copy (remove helper code, edit the `Settings` file) and pack it for release, and the original project is not touched. + +The package has one public class, [**ProjPacker**](ProjPacker/), with three methods: **Unpack** and **Pack**, and [**Test**](ProjPacker/Test), which runs the package's own tests. A project that references the package sees nothing else. The code that reads and writes the file format, the Windows calls under it and the tests are in private modules and classes. This section documents version 1.0.5.0, published under the MIT licence. + +```tb check_build +Dim pp As New ProjPacker +Dim work As String +work = Environ$("TEMP") & "\MyApp-release" + +' Unpack a copy of the project into a folder that is emptied first. +If pp.Unpack(App.Path & "\MyApp.twinproj", work, True) Then + ' ... change files in the folder here ... + If Not pp.Pack(work, App.Path & "\MyApp-release.twinproj", True) Then + Debug.Print "Pack failed" + End If +Else + Debug.Print "Unpack failed" +End If +``` + +> [!NOTE] +> The package reads and writes the binary [file format](../../../../Features/Packages/File-Format) itself, as the Import/export tool's script does, and the format is an internal detail of the IDE that can change. [**Test**](ProjPacker/Test) with a project file as its argument parses that file, writes it again and compares the result, so it shows whether the package still understands the files of a given twinBASIC build. + +## Output and errors + +No method raises an error. Each returns **True** when it did its work and **False** when it did not, and prints what happened with [**Debug.Print**](../../../Modules/Debug). A log file can receive the same lines. [**ProjPacker**](ProjPacker/#results-and-errors) describes both. + +Some calls are refused, and return **False**, because they would lose data or produce a project that opens wrongly: + +- **Pack** does not replace an existing file unless *overwrite* is **True**. +- **Pack** does not pack a folder that has no `Settings` file, because the packed file would open without its references, name and version. +- **Unpack** with *cleanFirst* does not empty a folder that is a drive root or directly under one, or that is not empty and does not look like an unpacked project. + +## Compared with the Import/export tool + +The tool's script and the package read and write the same file format. They differ in what they do around it: + +- The tool's `import` converts LF line endings to CRLF in `.twin`, `.bas` and `.cls` files. **Pack** stores every file as it is. +- The script skips a `.git` folder. **Pack** packs it, with every other hidden file and empty folder. +- The script adds the folders that the IDE always writes (`ImportedTypeLibraries`, `Miscellaneous`, `Packages`, `Resources` and `Sources`) when the folder lacks them. **Pack** adds none and prints a note naming the missing ones. +- The tool's `export` refuses to replace files already in the folder unless `--overwrite` is given. **Unpack** replaces them without asking, and has *cleanFirst* to empty the folder first. + +## Classes + +- [ProjPacker](ProjPacker/) -- unpacks a project file into a folder, packs a folder into a project file, and runs the package's tests + - [Pack](ProjPacker/Pack) -- packs a folder into a project file + - [Test](ProjPacker/Test) -- runs the package's tests + - [Unpack](ProjPacker/Unpack) -- unpacks a project file into a folder + +## See Also + +- [Import/export tool](../../../../Features/Packages/Import-Export-Tool) -- the command-line programs that do the same +- [Keeping a project in Git](../../../../Features/Packages/Import-Export-Tool#keeping-a-project-in-git) -- why and how to keep the unpacked folder under version control +- [TWINPACK file format](../../../../Features/Packages/File-Format) -- the container that both project file types use diff --git a/docs/_book.yml b/docs/_book.yml index 259aa89b..845e18b8 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -532,6 +532,13 @@ parts: landing_is_target: true outline_closed: true + - title: tbProjPacker Package + subtitle: Project files unpacked into source folders, and packed again + landing_page: /tB/Packages/Contributed/tbProjPacker/ + page: /tB/Packages/Contributed/tbProjPacker/ + landing_is_target: true + outline_closed: true + - title: tbRegExp Package subtitle: Regular expressions, in place of VBScript's RegExp object landing_page: /tB/Packages/Contributed/tbRegExp/ diff --git a/indexer/snapshots/contributed/tbProjPacker/api.json b/indexer/snapshots/contributed/tbProjPacker/api.json new file mode 100644 index 00000000..12c18cd0 --- /dev/null +++ b/indexer/snapshots/contributed/tbProjPacker/api.json @@ -0,0 +1,513 @@ +{ + "format": 3, + "package": "tbProjPacker", + "project": "tbProjPacker", + "version": "1.0.5.0", + "types": [ + { + "name": "ByteBuffer", + "kind": "class", + "vis": "private", + "public": false, + "file": "ByteBuffer.twin", + "signature": "Private Class ByteBuffer", + "members": [ + {"name":"Class_Initialize","kind":"sub","vis":"private","signature":"Private Sub Class_Initialize()"}, + {"name":"EnsureRoom","kind":"sub","vis":"private","signature":"Private Sub EnsureRoom(ByVal extra As Long)"}, + {"name":"INITIAL_CAPACITY","kind":"const","vis":"private","signature":"Private Const INITIAL_CAPACITY As Long = 65536"}, + {"name":"Length","kind":"property","vis":"public","signature":"Public Property Get Length() As Long","returnType":"Long","accessor":"Get"}, + {"name":"PutByte","kind":"sub","vis":"public","signature":"Public Sub PutByte(ByVal v As Byte)"}, + {"name":"PutBytes","kind":"sub","vis":"public","signature":"Public Sub PutBytes(ByRef b() As Byte, ByVal count As Long)"}, + {"name":"PutInt16","kind":"sub","vis":"public","signature":"Public Sub PutInt16(ByVal v As Integer)"}, + {"name":"PutLong","kind":"sub","vis":"public","signature":"Public Sub PutLong(ByVal v As Long)"}, + {"name":"PutLongLong","kind":"sub","vis":"public","signature":"Public Sub PutLongLong(ByVal v As LongLong)"}, + {"name":"ToArray","kind":"function","vis":"public","signature":"Public Function ToArray() As Byte()","returnType":"Byte()"}, + {"name":"m","kind":"field","vis":"private","signature":"Private m() As Byte"}, + {"name":"mCap","kind":"field","vis":"private","signature":"Private mCap As Long"}, + {"name":"mLen","kind":"field","vis":"private","signature":"Private mLen As Long"} + ] + }, + { + "name": "ByteReader", + "kind": "class", + "vis": "private", + "public": false, + "file": "ByteReader.twin", + "signature": "Private Class ByteReader", + "members": [ + {"name":"ErrorText","kind":"property","vis":"public","signature":"Public Property Get ErrorText() As String","returnType":"String","accessor":"Get"}, + {"name":"Init","kind":"sub","vis":"public","signature":"Public Sub Init(ByRef buf() As Byte, ByVal length As Long)"}, + {"name":"Need","kind":"sub","vis":"private","signature":"Private Sub Need(ByVal count As Long)"}, + {"name":"Position","kind":"property","vis":"public","signature":"Public Property Get Position() As Long","returnType":"Long","accessor":"Get"}, + {"name":"ReadBlob","kind":"sub","vis":"public","signature":"Public Sub ReadBlob(ByRef outBuf() As Byte, ByRef outLen As Long)"}, + {"name":"ReadByte","kind":"function","vis":"public","signature":"Public Function ReadByte() As Byte","returnType":"Byte"}, + {"name":"ReadInt16","kind":"function","vis":"public","signature":"Public Function ReadInt16() As Integer","returnType":"Integer"}, + {"name":"ReadLong","kind":"function","vis":"public","signature":"Public Function ReadLong() As Long","returnType":"Long"}, + {"name":"ReadLongLong","kind":"function","vis":"public","signature":"Public Function ReadLongLong() As LongLong","returnType":"LongLong"}, + {"name":"ReadString","kind":"function","vis":"public","signature":"Public Function ReadString() As String","returnType":"String"}, + {"name":"m","kind":"field","vis":"private","signature":"Private m() As Byte"}, + {"name":"mErrorText","kind":"field","vis":"private","signature":"Private mErrorText As String"}, + {"name":"mLen","kind":"field","vis":"private","signature":"Private mLen As Long"}, + {"name":"mPos","kind":"field","vis":"private","signature":"Private mPos As Long"} + ] + }, + { + "name": "Entry", + "kind": "class", + "vis": "private", + "public": false, + "file": "Entry.twin", + "signature": "Private Class Entry", + "members": [ + {"name":"AddChild","kind":"function","vis":"public","signature":"Public Function AddChild(ByVal child As Entry) As Entry","returnType":"Entry"}, + {"name":"AllocRevisions","kind":"sub","vis":"public","signature":"Public Sub AllocRevisions(ByVal count As Long)"}, + {"name":"Category","kind":"field","vis":"public","signature":"Public Category As Byte"}, + {"name":"Children","kind":"field","vis":"public","signature":"Public Children As Collection"}, + {"name":"Class_Initialize","kind":"sub","vis":"private","signature":"Private Sub Class_Initialize()"}, + {"name":"ContentByte","kind":"property","vis":"public","signature":"Public Property Get ContentByte(ByVal index As Long) As Byte","returnType":"Byte","accessor":"Get"}, + {"name":"ContentLen","kind":"property","vis":"public","signature":"Public Property Get ContentLen() As Long","returnType":"Long","accessor":"Get"}, + {"name":"Flags","kind":"field","vis":"public","signature":"Public Flags As Long"}, + {"name":"GetContent","kind":"function","vis":"public","signature":"Public Function GetContent() As Byte()","returnType":"Byte()"}, + {"name":"Kind","kind":"field","vis":"public","signature":"Public Kind As EntryKindEnum"}, + {"name":"Name","kind":"field","vis":"public","signature":"Public Name As String"}, + {"name":"Revision","kind":"field","vis":"public","signature":"Public Revision As LongLong"}, + {"name":"RevisionCount","kind":"property","vis":"public","signature":"Public Property Get RevisionCount() As Long","returnType":"Long","accessor":"Get"}, + {"name":"RevisionItem","kind":"property","vis":"public","signature":"Public Property Get RevisionItem(ByVal index As Long) As Long","returnType":"Long","accessor":"Get"}, + {"name":"RevisionItem","kind":"property","vis":"public","signature":"Public Property Let RevisionItem(ByVal index As Long, ByVal value As Long)","accessor":"Let"}, + {"name":"SetContent","kind":"sub","vis":"public","signature":"Public Sub SetContent(ByRef b() As Byte, ByVal length As Long)"}, + {"name":"mContent","kind":"field","vis":"private","signature":"Private mContent() As Byte"}, + {"name":"mContentLen","kind":"field","vis":"private","signature":"Private mContentLen As Long"}, + {"name":"mRevisionCount","kind":"field","vis":"private","signature":"Private mRevisionCount As Long"}, + {"name":"mRevisions","kind":"field","vis":"private","signature":"Private mRevisions() As Long"} + ] + }, + { + "name": "ProjPacker", + "kind": "class", + "vis": "public", + "file": "ProjPacker.twin", + "signature": "Public Class ProjPacker", + "members": [ + {"name":"Pack","kind":"function","vis":"public","signature":"Public Function Pack(ByVal inputDir As String, ByVal outputFile As String, Optional ByVal overwrite As Boolean = False, Optional ByVal logFile As String = \"\") As Boolean","returnType":"Boolean"}, + {"name":"RunGuarded","kind":"function","vis":"private","signature":"Private Function RunGuarded(ByVal opName As String, ByVal logFile As String, ByVal arg1 As String, ByVal arg2 As String, ByVal overwrite As Boolean, ByVal cleanFirst As Boolean, ByVal kind As OpKind) As Boolean","returnType":"Boolean"}, + {"name":"Test","kind":"function","vis":"public","signature":"Public Function Test(ByVal samplePath As String, Optional ByVal logFile As String = \"\") As Boolean","returnType":"Boolean"}, + {"name":"Unpack","kind":"function","vis":"public","signature":"Public Function Unpack(ByVal inputFile As String, ByVal outputDir As String, Optional ByVal cleanFirst As Boolean = False, Optional ByVal logFile As String = \"\") As Boolean","returnType":"Boolean"} + ] + }, + { + "name": "OpKind", + "kind": "enum", + "in": "ProjPacker", + "vis": "private", + "public": false, + "file": "ProjPacker.twin", + "signature": "Private Enum OpKind", + "values": [ + {"name":"opPack","value":"1"}, + {"name":"opUnpack","value":"2"}, + {"name":"opTest","value":"3"} + ] + }, + { + "name": "modOutput", + "kind": "module", + "vis": "private", + "public": false, + "file": "modOutput.twin", + "signature": "Private Module modOutput", + "members": [ + {"name":"LogClose","kind":"sub","vis":"public","signature":"Public Sub LogClose()"}, + {"name":"LogOpen","kind":"sub","vis":"public","signature":"Public Sub LogOpen(ByVal path As String)"}, + {"name":"Say","kind":"sub","vis":"public","signature":"Public Sub Say(ByVal s As String)"}, + {"name":"mLogHandle","kind":"field","vis":"private","signature":"Private mLogHandle As LongPtr"}, + {"name":"mLogOpen","kind":"field","vis":"private","signature":"Private mLogOpen As Boolean"} + ] + }, + { + "name": "modPackUnpack", + "kind": "module", + "vis": "private", + "public": false, + "file": "modImpExp.twin", + "signature": "Private Module modPackUnpack", + "members": [ + {"name":"BuildTree","kind":"function","vis":"private","signature":"Private Function BuildTree(ByVal dirPath As String) As Entry","returnType":"Entry"}, + {"name":"CategoryFor","kind":"function","vis":"public","signature":"Public Function CategoryFor(ByVal name As String) As Byte","returnType":"Byte"}, + {"name":"CleanTargetForImport","kind":"sub","vis":"private","signature":"Private Sub CleanTargetForImport(ByVal outputDir As String)"}, + {"name":"CountEntry","kind":"sub","vis":"private","signature":"Private Sub CountEntry(ByVal e As Entry, ByRef res As PP_RESULT)"}, + {"name":"DoPack","kind":"function","vis":"public","signature":"Public Function DoPack(ByVal inputDir As String, ByVal outputPath As String, ByVal quiet As Boolean, Optional ByVal requireSettings As Boolean = True, Optional ByVal overwrite As Boolean = False) As PP_RESULT","returnType":"PP_RESULT"}, + {"name":"DoUnpack","kind":"function","vis":"public","signature":"Public Function DoUnpack(ByVal inputPath As String, ByVal outputDir As String, ByVal quiet As Boolean, Optional ByVal cleanFirst As Boolean = False) As PP_RESULT","returnType":"PP_RESULT"}, + {"name":"ExtractEntry","kind":"sub","vis":"private","signature":"Private Sub ExtractEntry(ByVal e As Entry, ByVal parentDir As String, ByRef res As PP_RESULT)"}, + {"name":"FindMissingCategoryFolders","kind":"function","vis":"private","signature":"Private Function FindMissingCategoryFolders(ByVal root As Entry) As String","returnType":"String"}, + {"name":"HasTopLevelEntry","kind":"function","vis":"private","signature":"Private Function HasTopLevelEntry(ByVal root As Entry, ByVal name As String) As Boolean","returnType":"Boolean"}, + {"name":"HasTopLevelFile","kind":"function","vis":"private","signature":"Private Function HasTopLevelFile(ByVal root As Entry, ByVal name As String) As Boolean","returnType":"Boolean"}, + {"name":"PP_CATEGORY_DEFAULT","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_DEFAULT As Byte = &H0"}, + {"name":"PP_CATEGORY_IMPORTED_TYPELIBS","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_IMPORTED_TYPELIBS As Byte = &H5"}, + {"name":"PP_CATEGORY_MISCELLANEOUS","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_MISCELLANEOUS As Byte = &H6"}, + {"name":"PP_CATEGORY_PACKAGES","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_PACKAGES As Byte = &H7"}, + {"name":"PP_CATEGORY_REFERENCES","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_REFERENCES As Byte = &H1"}, + {"name":"PP_CATEGORY_RESOURCES","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_RESOURCES As Byte = &H2"}, + {"name":"PP_CATEGORY_SETTINGS","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_SETTINGS As Byte = &H4"}, + {"name":"PP_CATEGORY_SOURCES","kind":"const","vis":"public","signature":"Public Const PP_CATEGORY_SOURCES As Byte = &H3"}, + {"name":"PP_ERR_BAD_MAGIC","kind":"const","vis":"public","signature":"Public Const PP_ERR_BAD_MAGIC As Long = vbObjectError + 1001"}, + {"name":"PP_ERR_BAD_UTF8","kind":"const","vis":"public","signature":"Public Const PP_ERR_BAD_UTF8 As Long = vbObjectError + 1004"}, + {"name":"PP_ERR_BAD_VERSION","kind":"const","vis":"public","signature":"Public Const PP_ERR_BAD_VERSION As Long = vbObjectError + 1002"}, + {"name":"PP_ERR_IO","kind":"const","vis":"public","signature":"Public Const PP_ERR_IO As Long = vbObjectError + 1005"}, + {"name":"PP_ERR_NO_SETTINGS","kind":"const","vis":"public","signature":"Public Const PP_ERR_NO_SETTINGS As Long = vbObjectError + 1006"}, + {"name":"PP_ERR_SHALLOW_TARGET","kind":"const","vis":"public","signature":"Public Const PP_ERR_SHALLOW_TARGET As Long = vbObjectError + 1008"}, + {"name":"PP_ERR_TARGET_EXISTS","kind":"const","vis":"public","signature":"Public Const PP_ERR_TARGET_EXISTS As Long = vbObjectError + 1007"}, + {"name":"PP_ERR_TRUNCATED","kind":"const","vis":"public","signature":"Public Const PP_ERR_TRUNCATED As Long = vbObjectError + 1003"}, + {"name":"PP_FLAGS_HIDDEN","kind":"const","vis":"public","signature":"Public Const PP_FLAGS_HIDDEN As Long = &H1"}, + {"name":"PP_FLAGS_NONE","kind":"const","vis":"public","signature":"Public Const PP_FLAGS_NONE As Long = &H0"}, + {"name":"PP_FLAGS_SUPER_HIDDEN","kind":"const","vis":"public","signature":"Public Const PP_FLAGS_SUPER_HIDDEN As Long = &H2"}, + {"name":"PP_FLAGS_VIRTUAL","kind":"const","vis":"public","signature":"Public Const PP_FLAGS_VIRTUAL As Long = &H4"}, + {"name":"PP_FORMAT_VERSION","kind":"const","vis":"public","signature":"Public Const PP_FORMAT_VERSION As Integer = 1"}, + {"name":"PP_MAGIC","kind":"const","vis":"public","signature":"Public Const PP_MAGIC As Long = &HEA0BA51C"}, + {"name":"ParseBuffer","kind":"function","vis":"public","signature":"Public Function ParseBuffer(ByRef data() As Byte, ByVal length As Long) As Entry","returnType":"Entry"}, + {"name":"ReadEntry","kind":"function","vis":"private","signature":"Private Function ReadEntry(ByVal rd As ByteReader, ByRef entryCount As Long) As Entry","returnType":"Entry"}, + {"name":"SerializeTree","kind":"function","vis":"public","signature":"Public Function SerializeTree(ByVal root As Entry, ByRef outLen As Long) As Byte()","returnType":"Byte()"}, + {"name":"WellKnownCategoryNames","kind":"function","vis":"private","signature":"Private Function WellKnownCategoryNames() As String","returnType":"String"}, + {"name":"WriteEntry","kind":"sub","vis":"private","signature":"Private Sub WriteEntry(ByVal bb As ByteBuffer, ByVal e As Entry, ByRef isFirst As Boolean)"}, + {"name":"WriteStr","kind":"sub","vis":"private","signature":"Private Sub WriteStr(ByVal bb As ByteBuffer, ByVal s As String)"} + ] + }, + { + "name": "EntryKindEnum", + "kind": "enum", + "in": "modPackUnpack", + "vis": "public", + "file": "modImpExp.twin", + "signature": "Public Enum EntryKindEnum", + "values": [ + {"name":"ekFile","value":"1"}, + {"name":"ekDirectory","value":"2"} + ] + }, + { + "name": "PP_RESULT", + "kind": "type", + "in": "modPackUnpack", + "vis": "public", + "public": false, + "file": "modImpExp.twin", + "signature": "Public Type PP_RESULT", + "fields": [ + {"name":"Name","vis":"public","signature":"Name As String"}, + {"name":"Size","vis":"public","signature":"Size As Long"}, + {"name":"FileCount","vis":"public","signature":"FileCount As Long"}, + {"name":"DirCount","vis":"public","signature":"DirCount As Long"} + ], + "members": [] + }, + { + "name": "modSelfTest", + "kind": "module", + "vis": "private", + "public": false, + "file": "modSelfTest.twin", + "signature": "Private Module modSelfTest", + "members": [ + {"name":"AssertEqL","kind":"sub","vis":"private","signature":"Private Sub AssertEqL(ByVal actual As Long, ByVal expected As Long, ByVal msg As String)"}, + {"name":"AssertEqS","kind":"sub","vis":"private","signature":"Private Sub AssertEqS(ByVal actual As String, ByVal expected As String, ByVal msg As String)"}, + {"name":"AssertTrue","kind":"sub","vis":"private","signature":"Private Sub AssertTrue(ByVal cond As Boolean, ByVal msg As String)"}, + {"name":"BytesEqual","kind":"function","vis":"private","signature":"Private Function BytesEqual(ByRef a() As Byte, ByVal na As Long, ByRef b() As Byte, ByVal nb As Long) As Boolean","returnType":"Boolean"}, + {"name":"CountRoot","kind":"sub","vis":"private","signature":"Private Sub CountRoot(ByVal root As Entry, ByRef fileCount As Long, ByRef dirCount As Long)"}, + {"name":"CountTree","kind":"sub","vis":"private","signature":"Private Sub CountTree(ByVal e As Entry, ByRef fileCount As Long, ByRef dirCount As Long)"}, + {"name":"EntryContentEqual","kind":"function","vis":"private","signature":"Private Function EntryContentEqual(ByVal x As Entry, ByVal y As Entry) As Boolean","returnType":"Boolean"}, + {"name":"Fail","kind":"sub","vis":"private","signature":"Private Sub Fail(ByVal msg As String)"}, + {"name":"FlattenTree","kind":"sub","vis":"private","signature":"Private Sub FlattenTree(ByVal e As Entry, ByVal prefix As String, ByVal paths As Collection, ByVal entries As Collection)"}, + {"name":"Info","kind":"sub","vis":"private","signature":"Private Sub Info(ByVal msg As String)"}, + {"name":"MakeProjectTree","kind":"function","vis":"private","signature":"Private Function MakeProjectTree(ByVal name As String, ByVal withSettings As Boolean) As String","returnType":"String"}, + {"name":"MakeShapedFolder","kind":"function","vis":"private","signature":"Private Function MakeShapedFolder(ByVal name As String, ByVal settingsForm As Long) As String","returnType":"String"}, + {"name":"RunSelfTest","kind":"function","vis":"public","signature":"Public Function RunSelfTest(ByVal samplePath As String) As Long","returnType":"Long"}, + {"name":"RunTest","kind":"sub","vis":"private","signature":"Private Sub RunTest(ByVal id As Long, ByVal name As String, ByVal needsSample As Boolean)"}, + {"name":"T_BadMagicRejected","kind":"sub","vis":"private","signature":"Private Sub T_BadMagicRejected()"}, + {"name":"T_CleanAllowsEmptyFolder","kind":"sub","vis":"private","signature":"Private Sub T_CleanAllowsEmptyFolder()"}, + {"name":"T_CleanAllowsProject","kind":"sub","vis":"private","signature":"Private Sub T_CleanAllowsProject()"}, + {"name":"T_CleanFirstRemovesStale","kind":"sub","vis":"private","signature":"Private Sub T_CleanFirstRemovesStale()"}, + {"name":"T_CleanRefusesNonProject","kind":"sub","vis":"private","signature":"Private Sub T_CleanRefusesNonProject()"}, + {"name":"T_CleanRefusesSettingsFolder","kind":"sub","vis":"private","signature":"Private Sub T_CleanRefusesSettingsFolder()"}, + {"name":"T_DiskRoundTrip","kind":"sub","vis":"private","signature":"Private Sub T_DiskRoundTrip()"}, + {"name":"T_EmptyProject","kind":"sub","vis":"private","signature":"Private Sub T_EmptyProject()"}, + {"name":"T_ExportRefusesSettingsFolder","kind":"sub","vis":"private","signature":"Private Sub T_ExportRefusesSettingsFolder()"}, + {"name":"T_FlagsPreserved","kind":"sub","vis":"private","signature":"Private Sub T_FlagsPreserved()"}, + {"name":"T_Idempotence","kind":"sub","vis":"private","signature":"Private Sub T_Idempotence()"}, + {"name":"T_ImportMissingInput","kind":"sub","vis":"private","signature":"Private Sub T_ImportMissingInput()"}, + {"name":"T_InMemoryRoundTrip","kind":"sub","vis":"private","signature":"Private Sub T_InMemoryRoundTrip()"}, + {"name":"T_LongPath","kind":"sub","vis":"private","signature":"Private Sub T_LongPath()"}, + {"name":"T_OverwriteGuardFires","kind":"sub","vis":"private","signature":"Private Sub T_OverwriteGuardFires()"}, + {"name":"T_OverwritePassesWithFlag","kind":"sub","vis":"private","signature":"Private Sub T_OverwritePassesWithFlag()"}, + {"name":"T_ParseSample","kind":"sub","vis":"private","signature":"Private Sub T_ParseSample()"}, + {"name":"T_PathDepthGuard","kind":"sub","vis":"private","signature":"Private Sub T_PathDepthGuard()"}, + {"name":"T_SettingsGuardFires","kind":"sub","vis":"private","signature":"Private Sub T_SettingsGuardFires()"}, + {"name":"T_SettingsGuardPassesWithFile","kind":"sub","vis":"private","signature":"Private Sub T_SettingsGuardPassesWithFile()"}, + {"name":"T_SingleFile","kind":"sub","vis":"private","signature":"Private Sub T_SingleFile()"}, + {"name":"T_TruncationRejected","kind":"sub","vis":"private","signature":"Private Sub T_TruncationRejected()"}, + {"name":"T_UnicodeAndOrdering","kind":"sub","vis":"private","signature":"Private Sub T_UnicodeAndOrdering()"}, + {"name":"WalkDisk","kind":"sub","vis":"private","signature":"Private Sub WalkDisk(ByVal dirPath As String, ByVal prefix As String, ByVal paths As Collection)"}, + {"name":"mFailed","kind":"field","vis":"private","signature":"Private mFailed As Long"}, + {"name":"mHasSample","kind":"field","vis":"private","signature":"Private mHasSample As Boolean"}, + {"name":"mPassed","kind":"field","vis":"private","signature":"Private mPassed As Long"}, + {"name":"mSampleBuf","kind":"field","vis":"private","signature":"Private mSampleBuf() As Byte"}, + {"name":"mSampleLen","kind":"field","vis":"private","signature":"Private mSampleLen As Long"}, + {"name":"mSamplePath","kind":"field","vis":"private","signature":"Private mSamplePath As String"}, + {"name":"mSkipped","kind":"field","vis":"private","signature":"Private mSkipped As Long"}, + {"name":"mTmpDir","kind":"field","vis":"private","signature":"Private mTmpDir As String"} + ] + }, + { + "name": "modShared", + "kind": "module", + "vis": "private", + "public": false, + "file": "modShared.twin", + "signature": "Private Module modShared", + "members": [ + {"name":"AbsPath","kind":"function","vis":"public","signature":"Public Function AbsPath(ByVal p As String) As String","returnType":"String"}, + {"name":"ApiPath","kind":"function","vis":"public","signature":"Public Function ApiPath(ByVal canonicalAbsPath As String) As String","returnType":"String"}, + {"name":"BaseName","kind":"function","vis":"public","signature":"Public Function BaseName(ByVal p As String) As String","returnType":"String"}, + {"name":"DeleteTree","kind":"sub","vis":"public","signature":"Public Sub DeleteTree(ByVal dirPath As String)"}, + {"name":"EnsureDirectory","kind":"sub","vis":"public","signature":"Public Sub EnsureDirectory(ByVal p As String)"}, + {"name":"IsDirEmpty","kind":"function","vis":"public","signature":"Public Function IsDirEmpty(ByVal dirPath As String) As Boolean","returnType":"Boolean"}, + {"name":"IsDirectory","kind":"function","vis":"public","signature":"Public Function IsDirectory(ByVal p As String) As Boolean","returnType":"Boolean"}, + {"name":"IsFile","kind":"function","vis":"public","signature":"Public Function IsFile(ByVal p As String) As Boolean","returnType":"Boolean"}, + {"name":"JoinPath","kind":"function","vis":"public","signature":"Public Function JoinPath(ByVal a As String, ByVal b As String) As String","returnType":"String"}, + {"name":"ListDirectory","kind":"sub","vis":"public","signature":"Public Sub ListDirectory(ByVal dirPath As String, ByRef outNames() As String, ByRef outIsDir() As Boolean, ByRef outCount As Long)"}, + {"name":"OrdinalCompare","kind":"function","vis":"public","signature":"Public Function OrdinalCompare(ByVal a As String, ByVal b As String) As Long","returnType":"Long"}, + {"name":"PP_LONGPATH_THRESHOLD","kind":"const","vis":"private","signature":"Private Const PP_LONGPATH_THRESHOLD As Long = 248"}, + {"name":"ParentPath","kind":"function","vis":"public","signature":"Public Function ParentPath(ByVal p As String) As String","returnType":"String"}, + {"name":"PathDepth","kind":"function","vis":"public","signature":"Public Function PathDepth(ByVal p As String) As Long","returnType":"Long"}, + {"name":"PathExists","kind":"function","vis":"public","signature":"Public Function PathExists(ByVal p As String) As Boolean","returnType":"Boolean"}, + {"name":"ReadWholeFile","kind":"sub","vis":"public","signature":"Public Sub ReadWholeFile(ByVal path As String, ByRef outBuf() As Byte, ByRef outLen As Long)"}, + {"name":"SortStrings","kind":"sub","vis":"public","signature":"Public Sub SortStrings(ByRef arr() As String, ByVal count As Long)"}, + {"name":"Utf8Decode","kind":"function","vis":"public","signature":"Public Function Utf8Decode(ByRef b() As Byte, ByVal offset As Long, ByVal length As Long) As String","returnType":"String"}, + {"name":"Utf8Encode","kind":"sub","vis":"public","signature":"Public Sub Utf8Encode(ByVal s As String, ByRef outBuf() As Byte, ByRef outLen As Long)"}, + {"name":"WCharBufToStr","kind":"function","vis":"private","signature":"Private Function WCharBufToStr(ByRef firstChar As Integer, ByVal maxChars As Long) As String","returnType":"String"}, + {"name":"WriteWholeFile","kind":"sub","vis":"public","signature":"Public Sub WriteWholeFile(ByVal path As String, ByRef buf() As Byte, ByVal length As Long)"} + ] + }, + { + "name": "modTest", + "kind": "module", + "vis": "private", + "public": false, + "file": "Examples/modTest.twin", + "signature": "Private Module modTest", + "members": [ + {"name":"PackTwinProj","kind":"sub","vis":"private","signature":"Private Sub PackTwinProj()"}, + {"name":"UnpackTwinProj","kind":"sub","vis":"private","signature":"Private Sub UnpackTwinProj()"} + ] + }, + { + "name": "modWin32", + "kind": "module", + "vis": "private", + "public": false, + "file": "modWin32.twin", + "signature": "Private Module modWin32", + "members": [ + {"name":"CREATE_ALWAYS","kind":"const","vis":"public","signature":"Public Const CREATE_ALWAYS As Long = 2"}, + {"name":"CloseHandle","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function CloseHandle Lib \"kernel32\" (ByVal hObject As LongPtr) As BOOL","returnType":"BOOL"}, + {"name":"CopyMemory","kind":"sub","vis":"public","declare":true,"signature":"Public Declare PtrSafe Sub CopyMemory Lib \"kernel32\" Alias \"RtlMoveMemory\" (Destination As Any, Source As Any, ByVal Length As LongPtr)"}, + {"name":"CreateDirectoryW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function CreateDirectoryW Lib \"kernel32\" (ByVal lpPathName As LongPtr, lpSecurityAttributes As SECURITY_ATTRIBUTES) As BOOL","returnType":"BOOL"}, + {"name":"CreateFileW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function CreateFileW Lib \"kernel32\" (ByVal lpFileName As LongPtr, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, lpSecurityAttributes As SECURITY_ATTRIBUTES, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As LongPtr) As LongPtr","returnType":"LongPtr"}, + {"name":"FindClose","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function FindClose Lib \"kernel32\" (ByVal hFindFile As LongPtr) As Long","returnType":"Long"}, + {"name":"FindFirstFileW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function FindFirstFileW Lib \"kernel32\" (ByVal lpFileName As LongPtr, ByRef lpFindFileData As WIN32_FIND_DATAW) As LongPtr","returnType":"LongPtr"}, + {"name":"FindNextFileW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function FindNextFileW Lib \"kernel32\" (ByVal hFindFile As LongPtr, ByRef lpFindFileData As WIN32_FIND_DATAW) As Long","returnType":"Long"}, + {"name":"GENERIC_WRITE","kind":"const","vis":"public","signature":"Public Const GENERIC_WRITE As Long = &H40000000"}, + {"name":"GetFileAttributesW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function GetFileAttributesW Lib \"kernel32\" (ByVal lpFileName As LongPtr) As FILE_ATTRIBUTES","returnType":"FILE_ATTRIBUTES"}, + {"name":"GetFullPathNameW","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function GetFullPathNameW Lib \"kernel32\" (ByVal lpFileName As LongPtr, ByVal nBufferLength As Long, ByVal lpBuffer As LongPtr, lpFilePart As LongPtr) As Long","returnType":"Long"}, + {"name":"INVALID_HANDLE_VALUE","kind":"const","vis":"public","signature":"Public Const INVALID_HANDLE_VALUE As LongPtr = -1&"}, + {"name":"MAX_PATH","kind":"const","vis":"public","signature":"Public Const MAX_PATH As Long = 260"}, + {"name":"MultiByteToWideChar","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function MultiByteToWideChar Lib \"kernel32\" (ByVal CodePage As CodePageDefaults, ByVal dwFlags As MBWC_Flags, ByVal lpMultiByteStr As LongPtr, ByVal cchMultiByte As Long, ByVal lpWideCharStr As LongPtr, ByVal cchWideChar As Long) As Long","returnType":"Long"}, + {"name":"WideCharToMultiByte","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function WideCharToMultiByte Lib \"kernel32\" (ByVal CodePage As CodePageDefaults, ByVal dwFlags As WCMB_Flags, ByVal lpWideCharStr As LongPtr, ByVal cchWideChar As Long, ByVal lpMultiByteStr As LongPtr, ByVal cbMultiByte As Long, Optional ByVal lpDefaultChar As LongPtr, Optional lpUsedDefaultChar As BOOL) As Long","returnType":"Long"}, + {"name":"WriteFile","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function WriteFile Lib \"kernel32\" (ByVal hFile As LongPtr, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As OVERLAPPED) As BOOL","returnType":"BOOL"} + ] + }, + { + "name": "BOOL", + "kind": "enum", + "in": "modWin32", + "vis": "public", + "file": "modWin32.twin", + "signature": "Public Enum BOOL", + "values": [ + {"name":"CFALSE"}, + {"name":"CTRUE"}, + {"name":"API_TRUE","value":"1"}, + {"name":"API_FALSE","value":"0"}, + {"name":"APITRUE","value":"1"}, + {"name":"APIFALSE","value":"0"} + ] + }, + { + "name": "CodePageDefaults", + "kind": "enum", + "in": "modWin32", + "vis": "public", + "file": "modWin32.twin", + "signature": "Public Enum CodePageDefaults", + "values": [ + {"name":"CP_ACP","value":"0"}, + {"name":"CP_OEMCP","value":"1"}, + {"name":"CP_MACCP","value":"2"}, + {"name":"CP_THREAD_ACP","value":"3"}, + {"name":"CP_037","value":"37"}, + {"name":"CP_SYMBOL","value":"42"}, + {"name":"CP_SHIFTJIS","value":"932"}, + {"name":"CP_GB2312","value":"936"}, + {"name":"CP_BIG5","value":"950"}, + {"name":"CP_WINANSI","value":"100"}, + {"name":"CP_LATIN1","value":"1250"}, + {"name":"CP_UNICODE","value":"1200"}, + {"name":"CP_WINUNICODE","value":"1200"}, + {"name":"CP_WINNEUTRAL","value":"CP_WINUNICODE"}, + {"name":"CP_UTF16","value":"1200"}, + {"name":"CP_UTF16LE","value":"1200"}, + {"name":"CP_UTF16BE","value":"1201"}, + {"name":"CP_UTF32LE","value":"12000"}, + {"name":"CP_UTF32BE","value":"12001"}, + {"name":"CP_UTF16_LE","value":"1200"}, + {"name":"CP_UTF16_BE","value":"1201"}, + {"name":"CP_ASCII","value":"20127"}, + {"name":"CP_UTF7","value":"65000"}, + {"name":"CP_UTF8","value":"65001"} + ] + }, + { + "name": "FILETIME", + "kind": "type", + "in": "modWin32", + "vis": "public", + "public": false, + "file": "modWin32.twin", + "signature": "Public Type FILETIME", + "fields": [ + {"name":"dwLowDateTime","vis":"public","signature":"dwLowDateTime As Long"}, + {"name":"dwHighDateTime","vis":"public","signature":"dwHighDateTime As Long"} + ], + "members": [] + }, + { + "name": "FILE_ATTRIBUTES", + "kind": "enum", + "in": "modWin32", + "vis": "public", + "file": "modWin32.twin", + "signature": "Public Enum FILE_ATTRIBUTES", + "values": [ + {"name":"INVALID_FILE_ATTRIBUTES","value":"-1"}, + {"name":"FILE_ATTRIBUTE_READONLY","value":"&H00000001"}, + {"name":"FILE_ATTRIBUTE_HIDDEN","value":"&H00000002"}, + {"name":"FILE_ATTRIBUTE_SYSTEM","value":"&H00000004"}, + {"name":"FILE_ATTRIBUTE_DIRECTORY","value":"&H00000010"}, + {"name":"FILE_ATTRIBUTE_ARCHIVE","value":"&H00000020"}, + {"name":"FILE_ATTRIBUTE_DEVICE","value":"&H00000040"}, + {"name":"FILE_ATTRIBUTE_NORMAL","value":"&H00000080"}, + {"name":"FILE_ATTRIBUTE_TEMPORARY","value":"&H00000100"}, + {"name":"FILE_ATTRIBUTE_SPARSE_FILE","value":"&H00000200"}, + {"name":"FILE_ATTRIBUTE_REPARSE_POINT","value":"&H00000400"}, + {"name":"FILE_ATTRIBUTE_COMPRESSED","value":"&H00000800"}, + {"name":"FILE_ATTRIBUTE_OFFLINE","value":"&H00001000"}, + {"name":"FILE_ATTRIBUTE_NOT_CONTENT_INDEXED","value":"&H00002000"}, + {"name":"FILE_ATTRIBUTE_ENCRYPTED","value":"&H00004000"}, + {"name":"FILE_ATTRIBUTE_INTEGRITY_STREAM","value":"&H00008000&"}, + {"name":"FILE_ATTRIBUTE_VIRTUAL","value":"&H00010000"}, + {"name":"FILE_ATTRIBUTE_NO_SCRUB_DATA","value":"&H00020000"}, + {"name":"FILE_ATTRIBUTE_EA","value":"&H00040000"}, + {"name":"FILE_ATTRIBUTE_RECALL_ON_OPEN","value":"&H00040000"}, + {"name":"FILE_ATTRIBUTE_PINNED","value":"&H000080000"}, + {"name":"FILE_ATTRIBUTE_UNPINNED","value":"&H00100000"}, + {"name":"FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS","value":"&H400000"}, + {"name":"FILE_ATTRIBUTE_STRICTLY_SEQUENTIAL","value":"&H20000000"} + ] + }, + { + "name": "MBWC_Flags", + "kind": "enum", + "in": "modWin32", + "vis": "public", + "file": "modWin32.twin", + "signature": "Public Enum MBWC_Flags", + "values": [ + {"name":"MB_PRECOMPOSED","value":"&H1"}, + {"name":"MB_COMPOSITE","value":"&H2"}, + {"name":"MB_USEGLYPHCHARS","value":"&H4"}, + {"name":"MB_ERR_INVALID_CHARS","value":"&H8"} + ] + }, + { + "name": "OVERLAPPED", + "kind": "type", + "in": "modWin32", + "vis": "public", + "public": false, + "file": "modWin32.twin", + "signature": "Public Type OVERLAPPED", + "fields": [ + {"name":"Internal","vis":"public","signature":"Internal As LongPtr"}, + {"name":"InternalHigh","vis":"public","signature":"InternalHigh As LongPtr"}, + {"name":"OffsetsOrPtr","vis":"public","signature":"OffsetsOrPtr As LongLong"}, + {"name":"OffsetOrPtr","vis":"public","signature":"OffsetOrPtr As Long"}, + {"name":"OffsetHigh","vis":"public","signature":"OffsetHigh As Long"}, + {"name":"hEvent","vis":"public","signature":"hEvent As LongPtr"} + ], + "members": [] + }, + { + "name": "SECURITY_ATTRIBUTES", + "kind": "type", + "in": "modWin32", + "vis": "public", + "public": false, + "file": "modWin32.twin", + "signature": "Public Type SECURITY_ATTRIBUTES", + "fields": [ + {"name":"nLength","vis":"public","signature":"nLength As Long"}, + {"name":"lpSecurityDescriptor","vis":"public","signature":"lpSecurityDescriptor As LongPtr"}, + {"name":"bInheritHandle","vis":"public","signature":"bInheritHandle As Long"} + ], + "members": [] + }, + { + "name": "WCMB_Flags", + "kind": "enum", + "in": "modWin32", + "vis": "public", + "file": "modWin32.twin", + "signature": "Public Enum WCMB_Flags", + "values": [ + {"name":"WC_COMPOSITECHECK","value":"&H00000200"}, + {"name":"WC_DISCARDNS","value":"&H00000010"}, + {"name":"WC_SEPCHARS","value":"&H00000020"}, + {"name":"WC_DEFAULTCHAR","value":"&H00000040"}, + {"name":"WC_ERR_INVALID_CHARS","value":"&H00000080"}, + {"name":"WC_NO_BEST_FIT_CHARS","value":"&H00000400"} + ] + }, + { + "name": "WIN32_FIND_DATAW", + "kind": "type", + "in": "modWin32", + "vis": "public", + "public": false, + "file": "modWin32.twin", + "signature": "Public Type WIN32_FIND_DATAW", + "fields": [ + {"name":"dwFileAttributes","vis":"public","signature":"dwFileAttributes As FILE_ATTRIBUTES"}, + {"name":"ftCreationTime","vis":"public","signature":"ftCreationTime As FILETIME"}, + {"name":"ftLastAccessTime","vis":"public","signature":"ftLastAccessTime As FILETIME"}, + {"name":"ftLastWriteTime","vis":"public","signature":"ftLastWriteTime As FILETIME"}, + {"name":"nFileSizeHigh","vis":"public","signature":"nFileSizeHigh As Long"}, + {"name":"nFileSizeLow","vis":"public","signature":"nFileSizeLow As Long"}, + {"name":"dwReserved0","vis":"public","signature":"dwReserved0 As Long"}, + {"name":"dwReserved1","vis":"public","signature":"dwReserved1 As Long"}, + {"name":"cFileName","vis":"public","signature":"cFileName(0 To (MAX_PATH - 1)) As Integer"}, + {"name":"cAlternateFileName","vis":"public","signature":"cAlternateFileName(0 To 13) As Integer"} + ], + "members": [] + } + ], + "aliases": [], + "problems": [] +} diff --git a/test/example-projects/contributed-tbProjPacker/Settings b/test/example-projects/contributed-tbProjPacker/Settings new file mode 100644 index 00000000..36ae5b1c --- /dev/null +++ b/test/example-projects/contributed-tbProjPacker/Settings @@ -0,0 +1,74 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Documentation samples", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Template for scripts/check_examples.mjs. project.id, project.name and project.buildPath are rewritten in the staged copy, one per batch.", + "project.exportPathIsV2": true, + "project.id": "{7B247400-0000-4000-9000-7B2474000000}", + "project.name": "DocSamples", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "autoCtlExt": true, + "id": "{E89C8DEE-35ED-42A0-8274-449B6E5E5E3B}", + "licence": "MIT", + "name": "[IMPORTED] Project (Un)Packer for .twinproj and .twinpack Files", + "path32": "/Packages/tbProjPacker", + "path64": "/Packages/tbProjPacker", + "symbolId": "tbProjPacker", + "versionBuild": "0", + "versionMajor": "1", + "versionMinor": "0", + "versionRevision": "5" + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} From f8e122cdae740c8d3aa643e04844681a8c13aa5e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 23:02:51 +0200 Subject: [PATCH 6/6] TBMANLIB_Dialog: a contributed section of 32 pages (cDialog and its 30 members), with its snapshot, sample template and book chapter; what its dialogs do comes from a probe kept in test/package-probes/, which operates them in an exe on a private desktop --- WIP.md | 2 +- builder/page-baseline.json | 2 +- docs/Reference/Contributed.md | 1 + .../TBMANLIB_Dialog/cDialog/AddFilter.md | 55 ++ .../TBMANLIB_Dialog/cDialog/ClearFilter.md | 38 ++ .../TBMANLIB_Dialog/cDialog/CreatePrompt.md | 59 ++ .../TBMANLIB_Dialog/cDialog/DefaultExt.md | 61 ++ .../TBMANLIB_Dialog/cDialog/DialogTitle.md | 52 ++ .../TBMANLIB_Dialog/cDialog/FileMustExist.md | 60 ++ .../TBMANLIB_Dialog/cDialog/FileName.md | 60 ++ .../TBMANLIB_Dialog/cDialog/Filter.md | 55 ++ .../cDialog/GetFileExtension.md | 46 ++ .../TBMANLIB_Dialog/cDialog/GetFileName.md | 46 ++ .../cDialog/GetFileNameWithoutExt.md | 48 ++ .../TBMANLIB_Dialog/cDialog/GetFilePath.md | 48 ++ .../TBMANLIB_Dialog/cDialog/HideReadOnly.md | 49 ++ .../TBMANLIB_Dialog/cDialog/InitialDir.md | 57 ++ .../TBMANLIB_Dialog/cDialog/MultiSelect.md | 68 +++ .../TBMANLIB_Dialog/cDialog/NewDialogStyle.md | 61 ++ .../TBMANLIB_Dialog/cDialog/NoChangeDir.md | 54 ++ .../TBMANLIB_Dialog/cDialog/OpenFile.md | 77 +++ .../cDialog/OverwritePrompt.md | 52 ++ .../TBMANLIB_Dialog/cDialog/PathMustExist.md | 56 ++ .../TBMANLIB_Dialog/cDialog/ReadOnly.md | 50 ++ .../TBMANLIB_Dialog/cDialog/Reset.md | 44 ++ .../cDialog/SelectAndOpenFile.md | 56 ++ .../TBMANLIB_Dialog/cDialog/SetCodeFilters.md | 70 +++ .../cDialog/SetCommonFilters.md | 47 ++ .../cDialog/SetDocumentFilters.md | 54 ++ .../cDialog/SetImageFilters.md | 56 ++ .../cDialog/ShowBrowseForFolder.md | 67 +++ .../TBMANLIB_Dialog/cDialog/ShowOpen.md | 81 +++ .../TBMANLIB_Dialog/cDialog/ShowSave.md | 75 +++ .../TBMANLIB_Dialog/cDialog/index.md | 130 +++++ .../Contributed/TBMANLIB_Dialog/index.md | 53 ++ docs/_book.yml | 7 + .../contributed/TBMANLIB_Dialog/api.json | 204 +++++++ test/README.md | 3 + .../contributed-TBMANLIB_Dialog/Settings | 74 +++ test/package-probes/TBMANLIB_Dialog/README.md | 49 ++ test/package-probes/TBMANLIB_Dialog/probe.mjs | 127 ++++ .../TBMANLIB_Dialog/run-exe.mjs | 143 +++++ .../TBMANLIB_Dialog/src/Auto.twin | 471 +++++++++++++++ .../TBMANLIB_Dialog/src/Probe.twin | 550 ++++++++++++++++++ 44 files changed, 3516 insertions(+), 2 deletions(-) create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/AddFilter.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ClearFilter.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/CreatePrompt.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DefaultExt.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DialogTitle.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileMustExist.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileName.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Filter.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileExtension.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileName.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileNameWithoutExt.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFilePath.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/HideReadOnly.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/InitialDir.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/MultiSelect.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NewDialogStyle.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NoChangeDir.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OpenFile.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OverwritePrompt.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/PathMustExist.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ReadOnly.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Reset.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SelectAndOpenFile.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCodeFilters.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCommonFilters.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetDocumentFilters.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetImageFilters.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowBrowseForFolder.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowOpen.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowSave.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/index.md create mode 100644 docs/Reference/Contributed/TBMANLIB_Dialog/index.md create mode 100644 indexer/snapshots/contributed/TBMANLIB_Dialog/api.json create mode 100644 test/example-projects/contributed-TBMANLIB_Dialog/Settings create mode 100644 test/package-probes/TBMANLIB_Dialog/README.md create mode 100644 test/package-probes/TBMANLIB_Dialog/probe.mjs create mode 100644 test/package-probes/TBMANLIB_Dialog/run-exe.mjs create mode 100644 test/package-probes/TBMANLIB_Dialog/src/Auto.twin create mode 100644 test/package-probes/TBMANLIB_Dialog/src/Probe.twin diff --git a/WIP.md b/WIP.md index ee48b85f..b33505f9 100644 --- a/WIP.md +++ b/WIP.md @@ -33,7 +33,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Thirteen **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, tbProjPacker, tbRegExp and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modPackUnpack` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. +Fourteen **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: FileProps, Fmt, OLEGuids, WinReg, TwinTimerPackage, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, tbProjPacker, tbRegExp and VBA_JSON complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modPackUnpack` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. Each published one has a chapter in the book's Packages part. **BigNumberLib and WinReg are not published**: their packages do not compile on BETA 1005, so their pages are kept but excluded in `_config.yml`'s `exclude:` (owner, 2026-10-11), whose comment says how to bring one back. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 70ef24cd..641d7b81 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1099, + "pages": 1131, "staticFiles": 794 } diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md index 35aefcb0..bfecc3c4 100644 --- a/docs/Reference/Contributed.md +++ b/docs/Reference/Contributed.md @@ -16,6 +16,7 @@ Each package is versioned apart from twinBASIC, and its section says which versi - [**FileProps**](FileProps/) -- the Windows shell properties of a file, read and written - [**Fmt**](Fmt/) -- **StringFormat**, a string built from a format string and values, in the style of C#'s **String.Format** - [**OLEGuids**](OLEGuids/) -- OLE and COM interfaces and structures in twinBASIC source, in place of `OLEGuids.tlb` +- [**TBMANLIB_Dialog**](TBMANLIB_Dialog/) -- the **cDialog** class: the Open, Save and folder dialogs, with file filters and path helpers - [**TBMANLIB_ListBoxHScroll**](TBMANLIB_ListBoxHScroll/) -- a horizontal scroll bar for a **ListBox** - [**TBMANLIB_RES**](TBMANLIB_RES/) -- resources embedded in an executable, as bytes, strings, hex, Base64 or pictures - [**tbProjPacker**](tbProjPacker/) -- a `.twinproj` or `.twinpack` file unpacked into a folder of source files, and packed again diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/AddFilter.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/AddFilter.md new file mode 100644 index 00000000..f2783770 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/AddFilter.md @@ -0,0 +1,55 @@ +--- +title: AddFilter +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/AddFilter +has_toc: false +--- + +# AddFilter +{: .no_toc } + +Appends a description and a pattern to the [**Filter**](Filter) of the Open and Save As dialogs. + +Syntax: *object*.**AddFilter** ( *strDescription*, *strExtension* ) + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strDescription* +: *required* A **String**, the text that the dialog shows for the pair, such as `Text Files (*.txt)`. + +*strExtension* +: *required* A **String**, the pattern that selects the files, such as `*.txt`. Several patterns are separated by semicolons: `*.jpg;*.jpeg`. + +## Remarks + +**AddFilter** appends `description|pattern` to the text of **Filter**, with a `|` in front of it when **Filter** is not empty. The first pair added is the one that the dialog selects when it opens. + +A filter with one pair gives a file-type list with one item. Despite its name, *strExtension* is a pattern, and the class uses the text as it is: it adds no `*.` in front. Write the pattern in the form that the Windows documentation of `OPENFILENAME` gives, such as `*.txt`. + +The class does not check the arguments. A `|` in either one adds an element, so the later elements are no longer paired correctly. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.AddFilter "Text Files (*.txt)", "*.txt" +dlg.AddFilter "Pictures", "*.png;*.jpg" +Debug.Print dlg.Filter ' Text Files (*.txt)|*.txt|Pictures|*.png;*.jpg +``` + +A `|` in a description makes three elements out of two: + +```tb check_build check_run +Dim dlg As New cDialog +dlg.AddFilter "Text|Notes", "*.txt" +Debug.Print UBound(Split(dlg.Filter, "|")) + 1 ' 3 +``` + +## See Also + +- [Filter](Filter) property -- the whole list as one string +- [ClearFilter](ClearFilter) method -- empties the filter +- [SetCommonFilters](SetCommonFilters) method -- replaces the filter with a ready-made list +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ClearFilter.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ClearFilter.md new file mode 100644 index 00000000..72858c90 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ClearFilter.md @@ -0,0 +1,38 @@ +--- +title: ClearFilter +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ClearFilter +has_toc: false +--- + +# ClearFilter +{: .no_toc } + +Empties the [**Filter**](Filter) of the Open and Save As dialogs. + +Syntax: *object*.**ClearFilter** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**ClearFilter** sets **Filter** to an empty string, as `dlg.Filter = ""` does. The four `Set...Filters` methods call it first, so each of them replaces the filter. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.AddFilter "Text Files (*.txt)", "*.txt" +Debug.Print Len(dlg.Filter) > 0 ' True + +dlg.ClearFilter +Debug.Print Len(dlg.Filter) ' 0 +``` + +## See Also + +- [Filter](Filter) property -- the whole list as one string +- [AddFilter](AddFilter) method -- appends a pair +- [Reset](Reset) method -- clears the filter along with every other property +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/CreatePrompt.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/CreatePrompt.md new file mode 100644 index 00000000..21c981af --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/CreatePrompt.md @@ -0,0 +1,59 @@ +--- +title: CreatePrompt +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/CreatePrompt +has_toc: false +--- + +# CreatePrompt +{: .no_toc } + +Returns or sets whether the Open dialog asks permission before it accepts the name of a file that does not exist. + +Syntax: *object*.**CreatePrompt** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_CREATEPROMPT`. The default is **False**. + +## Remarks + +Only [**ShowOpen**](ShowOpen) uses the property. [**ShowSave**](ShowSave) never sets `OFN_CREATEPROMPT`, and [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores the property. + +When the user types the name of a file that does not exist, such as `nosuch.txt`, the box *This file doesn't exist. Create the file?* appears, with the buttons *Yes* and *No*. *Yes* closes the dialog and returns the path. *No* leaves the dialog open. This happens whether [**FileMustExist**](FileMustExist) is **True** or **False**, so the question takes the place of the *File not found* box that **FileMustExist** shows. + +The file is not created: neither the dialog nor the class creates it, and the program has to do so. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.CreatePrompt ' False + +dlg.CreatePrompt = True +Debug.Print dlg.CreatePrompt ' True +``` + +This example lets the user name a new file in the Open dialog and creates it when the user agrees. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.CreatePrompt = True +path = dlg.ShowOpen() +If Len(path) > 0 Then + If Len(Dir$(path)) = 0 Then + Open path For Output As #1 + Close #1 + End If +End If +``` + +## See Also + +- [FileMustExist](FileMustExist) property -- lets only names of existing files through +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DefaultExt.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DefaultExt.md new file mode 100644 index 00000000..c79e4012 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DefaultExt.md @@ -0,0 +1,61 @@ +--- +title: DefaultExt +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/DefaultExt +has_toc: false +--- + +# DefaultExt +{: .no_toc } + +Returns or sets the extension that Windows adds to a file name that the user types without one. + +Syntax: *object*.**DefaultExt** [ = *string* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*string* +: A **String**, an extension written without a period, such as `txt`. The default is an empty string. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) pass the text as the `lpstrDefExt` member of `OPENFILENAME`. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores it. + +Windows appends the extension to a name that the user types without one. Write the extension without a period. The class does not change the text, and does not take an extension from the selected pair of [**Filter**](Filter). + +In the Save As dialog, with the user typing `out`: + +| **DefaultExt** | **ShowSave** returns | +|---|---| +| `txt` | `\out.txt` | +| `.txt` | `\out..txt` | +| An empty string | `\out` | + +`` is the folder of the dialog. A period in **DefaultExt** is kept, and gives a name with two periods. An empty **DefaultExt** appends nothing. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print "[" & dlg.DefaultExt & "]" ' [] + +dlg.DefaultExt = "txt" +Debug.Print dlg.DefaultExt ' txt +``` + +This example shows the Save As dialog and adds `.txt` to a name typed without an extension. + +```tb check_build +Dim dlg As New cDialog +dlg.DefaultExt = "txt" +dlg.SetCommonFilters +Debug.Print dlg.ShowSave() +``` + +## See Also + +- [Filter](Filter) property -- the file-type list, which does not set the extension +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DialogTitle.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DialogTitle.md new file mode 100644 index 00000000..17b8608f --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/DialogTitle.md @@ -0,0 +1,52 @@ +--- +title: DialogTitle +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/DialogTitle +has_toc: false +--- + +# DialogTitle +{: .no_toc } + +Returns or sets the text that the dialogs show as their title, or in the folder dialog as an instruction above the tree of folders. + +Syntax: *object*.**DialogTitle** [ = *string* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*string* +: A **String**. The default is an empty string. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) pass the text as the `lpstrTitle` member of `OPENFILENAME`, the text of the title bar. With an empty **DialogTitle** the Open dialog is titled *Open* and the Save As dialog *Save As*. A text that is set replaces the title. + +[**ShowBrowseForFolder**](ShowBrowseForFolder) passes the text as the `lpszTitle` member of `BROWSEINFO`. The title bar of that dialog always reads *Browse For Folder* (*Browse for Folder* when [**NewDialogStyle**](NewDialogStyle) is **False**), and **DialogTitle** is shown as a line of text above the tree of folders, as an instruction to the user. With an empty **DialogTitle** there is no text there. + +## Example + +This example reads the starting value and sets a title. + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print "[" & dlg.DialogTitle & "]" ' [] + +dlg.DialogTitle = "Choose the log file" +Debug.Print dlg.DialogTitle ' Choose the log file +``` + +This example shows the Open dialog with that title. + +```tb check_build +Dim dlg As New cDialog +dlg.DialogTitle = "Choose the log file" +Debug.Print dlg.ShowOpen() +``` + +## See Also + +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [ShowBrowseForFolder](ShowBrowseForFolder) method -- shows the Browse For Folder dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileMustExist.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileMustExist.md new file mode 100644 index 00000000..5ccf31f3 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileMustExist.md @@ -0,0 +1,60 @@ +--- +title: FileMustExist +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/FileMustExist +has_toc: false +--- + +# FileMustExist +{: .no_toc } + +Returns or sets whether the Open dialog lets the user choose only the name of a file that exists. + +Syntax: *object*.**FileMustExist** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_FILEMUSTEXIST`. The default is **True**. + +## Remarks + +Only [**ShowOpen**](ShowOpen) uses the property. [**ShowSave**](ShowSave) never sets `OFN_FILEMUSTEXIST`, and Windows documents the flag for Open dialogs only, so **FileMustExist** has no effect on a Save As dialog. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores it too. + +With **True**, a name that the user types and that does not exist, such as `nosuch.txt`, makes Windows show the box *File not found. Check the file name and try again.*, and the Open dialog stays open. Windows documents that the flag also applies `OFN_PATHMUSTEXIST`. With **False**, **ShowOpen** returns the path of a file that does not exist, and does not create the file, so the caller has to be ready to meet a missing file. + +With [**CreatePrompt**](CreatePrompt) **True**, the question of that property is asked in place of the box, whether **FileMustExist** is **True** or **False**. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.FileMustExist ' True + +dlg.FileMustExist = False +Debug.Print dlg.FileMustExist ' False +``` + +This example lets the Open dialog return a name that does not exist, and creates the file when it is missing. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.FileMustExist = False +path = dlg.ShowOpen() +If Len(path) > 0 Then + If Len(Dir$(path)) = 0 Then + Open path For Output As #1 + Close #1 + End If +End If +``` + +## See Also + +- [PathMustExist](PathMustExist) property -- the check on the folder of the name +- [CreatePrompt](CreatePrompt) property -- asks before a name that does not exist is accepted +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileName.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileName.md new file mode 100644 index 00000000..b17b752f --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/FileName.md @@ -0,0 +1,60 @@ +--- +title: FileName +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/FileName +has_toc: false +--- + +# FileName +{: .no_toc } + +Returns or sets the file name that the Open and Save As dialogs show when they open, and holds the path that the last of those dialogs returned. + +Syntax: *object*.**FileName** [ = *string* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*string* +: A **String**, a file name or a full path. The default is an empty string. + +## Remarks + +**Before a dialog.** [**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) copy the text into the buffer that Windows uses for the `lpstrFile` member of `OPENFILENAME`, which initialises the File name box. The Save As dialog shows an empty box for an empty **FileName** and `x.txt` for `x.txt`. [**ShowSave**](ShowSave) pads the buffer with spaces, and the dialog does not show the padding. + +**After a dialog.** A **ShowSave** or **ShowOpen** that the user confirms stores the path it returns in **FileName**. For a [**MultiSelect**](MultiSelect) selection of several files that is the folder, not a file name. A cancelled dialog leaves **FileName** as it was, and [**ShowBrowseForFolder**](ShowBrowseForFolder) neither reads nor changes it. + +> [!NOTE] +> Because a confirmed dialog stores the full path, the next **ShowOpen** or **ShowSave** passes that path as the initial file name. Set **FileName** to an empty string, or call [**Reset**](Reset), to start without it. + +**Length.** The buffer of **ShowSave** holds 260 characters. A **FileName** of up to 259 characters opens the dialog normally. A **FileName** of exactly 260 characters shows no dialog and returns an empty string at once, without an error, as a cancel does. A **FileName** of 261 characters or more raises run-time error 5, *Invalid procedure call or argument*, before any dialog. The buffer of **ShowOpen** holds 260 characters, or 32,768 when **FileName** is not empty or [**MultiSelect**](MultiSelect) is **True**. A **FileName** of 32,768 characters or more is not used by **ShowOpen**. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.FileName = "C:\Reports\summary.txt" +Debug.Print dlg.FileName ' C:\Reports\summary.txt +Debug.Print dlg.GetFileName(dlg.FileName) ' summary.txt + +dlg.Reset +Debug.Print "[" & dlg.FileName & "]" ' [] +``` + +This example suggests a name in the Save As dialog and prints the path that the user confirms. + +```tb check_build +Dim dlg As New cDialog +dlg.FileName = "summary.txt" +If Len(dlg.ShowSave()) > 0 Then + Debug.Print "Saved as " & dlg.FileName +End If +``` + +## See Also + +- [InitialDir](InitialDir) property -- the folder in which the dialogs start +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [GetFileName](GetFileName) method -- the name part of a path +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Filter.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Filter.md new file mode 100644 index 00000000..ff71ac9c --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Filter.md @@ -0,0 +1,55 @@ +--- +title: Filter +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/Filter +has_toc: false +--- + +# Filter +{: .no_toc } + +Returns or sets the file-type list of the Open and Save As dialogs. + +Syntax: *object*.**Filter** [ = *string* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*string* +: A **String** of pairs, each a description and a pattern, separated by `|`: `description|pattern|description|pattern`. The default is an empty string, which is a list with no pairs. With an empty filter the dialog has no file-type list: there is no *Files of type* box. + +## Remarks + +The syntax is that of the **Filter** property of VB6's **CommonDialog** control. A pattern is a file name pattern with wildcards, such as `*.txt`. Several patterns of one pair are separated by semicolons: `*.jpg;*.jpeg`. + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) replace every `|` with a null character and add one more null character at the end, which is the layout of the `lpstrFilter` member of `OPENFILENAME`. The file-type list holds the descriptions in the order of the pairs, and the first pair is selected when the dialog opens (`nFilterIndex` is 1). The class does not report the pair that the user chooses. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores the property. + +The class does not check the string. A `|` inside a description or a pattern starts a new element, and moves every later element one place, so the pairs no longer fit. [**AddFilter**](AddFilter) and the four `Set...Filters` methods build the string in pairs, unless an argument of **AddFilter** contains a `|`. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*" +Debug.Print dlg.Filter ' Text Files (*.txt)|*.txt|All Files (*.*)|*.* + +Dim parts() As String +parts = Split(dlg.Filter, "|") +Debug.Print UBound(parts) + 1 ' 4 +``` + +This example offers two kinds of file in the Open dialog. + +```tb check_build +Dim dlg As New cDialog +dlg.Filter = "Pictures (*.png;*.jpg)|*.png;*.jpg|All Files (*.*)|*.*" +Debug.Print dlg.ShowOpen() +``` + +## See Also + +- [AddFilter](AddFilter) method -- appends a pair +- [ClearFilter](ClearFilter) method -- empties the filter +- [SetCommonFilters](SetCommonFilters) method -- replaces the filter with a ready-made list +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileExtension.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileExtension.md new file mode 100644 index 00000000..f1b4a04f --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileExtension.md @@ -0,0 +1,46 @@ +--- +title: GetFileExtension +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/GetFileExtension +has_toc: false +--- + +# GetFileExtension +{: .no_toc } + +Returns the part of a path from its last period, the period included. + +Syntax: *object*.**GetFileExtension** ( *strFullPath* ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strFullPath* +: *required* A **String**, a path or a file name. + +## Remarks + +**GetFileExtension** returns the extension with its period: `.txt` for `C:\Docs\summary.txt`. A string with no period gives an empty string. Of `archive.tar.gz` only `.gz` is returned, and a name that ends with a period gives `.`. + +> [!NOTE] +> The method searches the whole string for the last period, not the file name only. A period in a folder name is taken for the start of an extension when the file name has none: `C:\my.dir\notes` gives `.dir\notes`. [**GetFileNameWithoutExt**](GetFileNameWithoutExt) looks in the file name only, and gives `notes` for the same path. + +The method works on text only. It does not check that the file exists, and it reads no property of the object. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.GetFileExtension("C:\Docs\summary.txt") ' .txt +Debug.Print dlg.GetFileExtension("archive.tar.gz") ' .gz +Debug.Print dlg.GetFileExtension("report.") ' . +Debug.Print "[" & dlg.GetFileExtension("README") & "]" ' [] +Debug.Print dlg.GetFileExtension("C:\my.dir\notes") ' .dir\notes +``` + +## See Also + +- [GetFileNameWithoutExt](GetFileNameWithoutExt) method -- the name without its extension +- [GetFileName](GetFileName) method -- the part after the last backslash +- [GetFilePath](GetFilePath) method -- the part before the last backslash +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileName.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileName.md new file mode 100644 index 00000000..ff6e8fcf --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileName.md @@ -0,0 +1,46 @@ +--- +title: GetFileName +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/GetFileName +has_toc: false +--- + +# GetFileName +{: .no_toc } + +Returns the part of a path after its last backslash. + +Syntax: *object*.**GetFileName** ( *strFullPath* ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strFullPath* +: *required* A **String**, a path or a file name. + +## Remarks + +**GetFileName** returns the file name of a path, with its extension: the name in `C:\Docs\summary.txt` is `summary.txt`. + +- Only the backslash separates folders. A forward slash is an ordinary character, so a path written with forward slashes is returned unchanged. +- A string with no backslash is returned unchanged: `summary.txt` gives `summary.txt`. +- A string that ends with a backslash, such as `C:\Docs\`, gives an empty string. + +The method works on text only. It does not check that the file exists, and it reads no property of the object. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.GetFileName("C:\Docs\Reports\summary.txt") ' summary.txt +Debug.Print dlg.GetFileName("summary.txt") ' summary.txt +Debug.Print "[" & dlg.GetFileName("C:\Docs\") & "]" ' [] +Debug.Print dlg.GetFileName("C:/Docs/summary.txt") ' C:/Docs/summary.txt +``` + +## See Also + +- [GetFilePath](GetFilePath) method -- the part before the last backslash +- [GetFileNameWithoutExt](GetFileNameWithoutExt) method -- the name without its extension +- [GetFileExtension](GetFileExtension) method -- the part from the last period +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileNameWithoutExt.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileNameWithoutExt.md new file mode 100644 index 00000000..9658f32a --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFileNameWithoutExt.md @@ -0,0 +1,48 @@ +--- +title: GetFileNameWithoutExt +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/GetFileNameWithoutExt +has_toc: false +--- + +# GetFileNameWithoutExt +{: .no_toc } + +Returns the file name of a path without its folder and without its last extension. + +Syntax: *object*.**GetFileNameWithoutExt** ( *strFullPath* ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strFullPath* +: *required* A **String**, a path or a file name. + +## Remarks + +**GetFileNameWithoutExt** takes the part of the string after the last backslash, as [**GetFileName**](GetFileName) does, and removes everything from the last period of that part. `C:\Docs\summary.txt` gives `summary`. The period is searched in the file name only, so a period in a folder name has no effect: `C:\my.dir\notes` gives `notes`. + +- Only the last extension is removed: `archive.tar.gz` gives `archive.tar`. +- A name without a period is returned as it is. +- A name that ends with a period loses it: `report.` gives `report`. +- A name that starts with a period and has no other gives an empty string: `.gitignore` gives an empty string. + +The method works on text only. It does not check that the file exists, and it reads no property of the object. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.GetFileNameWithoutExt("C:\Docs\summary.txt") ' summary +Debug.Print dlg.GetFileNameWithoutExt("archive.tar.gz") ' archive.tar +Debug.Print dlg.GetFileNameWithoutExt("README") ' README +Debug.Print dlg.GetFileNameWithoutExt("C:\my.dir\notes") ' notes +Debug.Print "[" & dlg.GetFileNameWithoutExt(".gitignore") & "]" ' [] +``` + +## See Also + +- [GetFileName](GetFileName) method -- the part after the last backslash +- [GetFileExtension](GetFileExtension) method -- the part from the last period +- [GetFilePath](GetFilePath) method -- the part before the last backslash +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFilePath.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFilePath.md new file mode 100644 index 00000000..481ba53e --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/GetFilePath.md @@ -0,0 +1,48 @@ +--- +title: GetFilePath +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/GetFilePath +has_toc: false +--- + +# GetFilePath +{: .no_toc } + +Returns the part of a path before its last backslash. + +Syntax: *object*.**GetFilePath** ( *strFullPath* ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strFullPath* +: *required* A **String**, a path or a file name. + +## Remarks + +**GetFilePath** returns the folder of a file path, without a trailing backslash: the folder of `C:\Docs\summary.txt` is `C:\Docs`, and that of a file in the root of a drive is `C:`, without the backslash. + +- Only the backslash separates folders. A forward slash is an ordinary character, so a path written with forward slashes is returned unchanged. +- A string with no backslash is returned unchanged, not as an empty string. `summary.txt` gives `summary.txt`, so a result equal to the argument means that the argument had no folder. +- A string that ends with a backslash loses it: `C:\Docs\` gives `C:\Docs`. A string that starts with its only backslash gives an empty string. + +The method works on text only. It does not check that the folder exists, and it reads no property of the object. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.GetFilePath("C:\Docs\Reports\summary.txt") ' C:\Docs\Reports +Debug.Print dlg.GetFilePath("C:\summary.txt") ' C: +Debug.Print dlg.GetFilePath("summary.txt") ' summary.txt +Debug.Print dlg.GetFilePath("C:\Docs\") ' C:\Docs +Debug.Print dlg.GetFilePath("C:/Docs/summary.txt") ' C:/Docs/summary.txt +Debug.Print "[" & dlg.GetFilePath("\summary.txt") & "]" ' [] +``` + +## See Also + +- [GetFileName](GetFileName) method -- the part after the last backslash +- [GetFileExtension](GetFileExtension) method -- the part from the last period +- [GetFileNameWithoutExt](GetFileNameWithoutExt) method -- the name without its extension +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/HideReadOnly.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/HideReadOnly.md new file mode 100644 index 00000000..b053a786 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/HideReadOnly.md @@ -0,0 +1,49 @@ +--- +title: HideReadOnly +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/HideReadOnly +has_toc: false +--- + +# HideReadOnly +{: .no_toc } + +Returns or sets whether the Open dialog hides its read-only choice. + +Syntax: *object*.**HideReadOnly** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_HIDEREADONLY`. The default is **True**. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) both pass the flag, and Windows describes it as hiding the Read Only check box. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores it. + +The Open dialog that Windows shows has no Read Only check box. With **HideReadOnly** **False**, its *Open* button is a split button whose menu holds *Open* and *Open as read-only*. With the default **True**, it is a plain button. The class does not read the flags back from the dialog, so the choice that the user made is not reported. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.HideReadOnly ' True + +dlg.HideReadOnly = False +Debug.Print dlg.HideReadOnly ' False +``` + +This example shows the Open dialog with the *Open as read-only* choice. + +```tb check_build +Dim dlg As New cDialog +dlg.HideReadOnly = False +Debug.Print dlg.ShowOpen() +``` + +## See Also + +- [ReadOnly](ReadOnly) property -- sets the read-only flag of the Open dialog +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/InitialDir.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/InitialDir.md new file mode 100644 index 00000000..17adbd55 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/InitialDir.md @@ -0,0 +1,57 @@ +--- +title: InitialDir +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/InitialDir +has_toc: false +--- + +# InitialDir +{: .no_toc } + +Returns or sets the folder in which the Open and Save As dialogs start. + +Syntax: *object*.**InitialDir** [ = *string* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*string* +: A **String**, the path of a folder. The default is an empty string. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) pass the text as the `lpstrInitialDir` member of `OPENFILENAME`. The class does not check that the folder exists. Windows chooses the starting folder in the order that the [documentation of `lpstrInitialDir`](https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew) gives. + +[**ShowBrowseForFolder**](ShowBrowseForFolder) does not use **InitialDir**. Its dialog opens with the folder of the user's profile selected, whatever **InitialDir** holds. + +> [!NOTE] +> A **ShowBrowseForFolder** that the user confirms replaces **InitialDir** with the folder it returns, a path typed into the edit box of the dialog included. A **ShowOpen** or **ShowSave** after it is given that folder as its starting folder. Set **InitialDir** again if that is not wanted. + +VB6's **CommonDialog** control calls this property **InitDir**. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print "[" & dlg.InitialDir & "]" ' [] + +dlg.InitialDir = "C:\Reports" +Debug.Print dlg.InitialDir ' C:\Reports +``` + +This example starts the Save As dialog in a given folder. + +```tb check_build +Dim dlg As New cDialog +dlg.InitialDir = "C:\Reports" +dlg.DefaultExt = "txt" +Debug.Print dlg.ShowSave() +``` + +## See Also + +- [FileName](FileName) property -- the file name shown when the dialog opens +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [ShowBrowseForFolder](ShowBrowseForFolder) method -- writes the chosen folder to **InitialDir** +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/MultiSelect.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/MultiSelect.md new file mode 100644 index 00000000..4fee9d05 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/MultiSelect.md @@ -0,0 +1,68 @@ +--- +title: MultiSelect +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/MultiSelect +has_toc: false +--- + +# MultiSelect +{: .no_toc } + +Returns or sets whether the Open dialog lets the user select more than one file. + +Syntax: *object*.**MultiSelect** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_ALLOWMULTISELECT` for [**ShowOpen**](ShowOpen). The default is **False**. + +## Remarks + +Only [**ShowOpen**](ShowOpen) uses the property. [**ShowSave**](ShowSave) and [**ShowBrowseForFolder**](ShowBrowseForFolder) ignore it. + +With **MultiSelect** **True**, **ShowOpen** gives Windows a buffer of 32,768 characters, in place of 260. The class also always sets `OFN_EXPLORER`, so Windows fills the buffer with the folder, then the name of each selected file, each ended by a null character, and one more null character at the end. When the user selects one file, the buffer holds its full path, with no separator. + +> [!WARNING] +> When the user selects two or more files, **ShowOpen** returns a **String** that holds only the folder of the files. It does not return an array and it does not return the file names, and [**FileName**](FileName) becomes the folder too. No error is raised, so the result looks like a valid answer and the names of the files are lost. One selected file gives its full path, as a **String**. + +The source of **ShowOpen** has a branch that splits the names and returns an array of full paths. It never runs: the code cuts the buffer at its first null character, which is the end of the folder, before it looks for the separators between the names. A program can tell the two results apart, because the result for several files is the path of a folder. + +[**SelectAndOpenFile**](SelectAndOpenFile) passes the folder to [**OpenFile**](OpenFile), which opens it in File Explorer. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.MultiSelect ' False + +dlg.MultiSelect = True +Debug.Print dlg.MultiSelect ' True +``` + +This example lets the user select several files, and reports when only the folder came back. + +```tb check_build +Dim dlg As New cDialog +Dim result As String + +dlg.MultiSelect = True +dlg.SetImageFilters +result = dlg.ShowOpen() + +If Len(result) = 0 Then + Debug.Print "Cancelled" +ElseIf GetAttr(result) And vbDirectory Then + Debug.Print "Several files were selected; only the folder came back: " & result +Else + Debug.Print "One file: " & result +End If +``` + +## See Also + +- [ShowOpen](ShowOpen) method -- shows the Open dialog and returns the result +- [SelectAndOpenFile](SelectAndOpenFile) method -- opens the file that the dialog returns +- [FileName](FileName) property -- holds the result +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NewDialogStyle.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NewDialogStyle.md new file mode 100644 index 00000000..8d94e8cc --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NewDialogStyle.md @@ -0,0 +1,61 @@ +--- +title: NewDialogStyle +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/NewDialogStyle +has_toc: false +--- + +# NewDialogStyle +{: .no_toc } + +Returns or sets whether the Browse For Folder dialog uses the new style, which can be resized and has an edit box. + +Syntax: *object*.**NewDialogStyle** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** adds `BIF_USENEWUI` to the flags of [**ShowBrowseForFolder**](ShowBrowseForFolder). The default is **True**. + +## Remarks + +Only **ShowBrowseForFolder** uses the property. [**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) ignore it. + +`BIF_USENEWUI` is the constant `BIF_NEWDIALOGSTYLE Or BIF_EDITBOX`. With **True**, the dialog is titled *Browse For Folder*, can be resized, and has an edit box, in which the user can type the path of a folder, and a *Make New Folder* button. It opens with the folder of the user's profile selected, whatever [**InitialDir**](InitialDir) holds. A path typed into the edit box and confirmed with *OK* is returned, and is stored in **InitialDir**. + +With **False**, **ShowBrowseForFolder** sets only `BIF_RETURNONLYFSDIRS`, which it always sets, and the dialog has the old style: it is titled *Browse for Folder*, has no edit box and no *Make New Folder* button, selects *This PC* when it opens, and keeps *OK* disabled until the user selects a folder of the file system. + +The Windows documentation of `SHBrowseForFolder` requires COM to be initialised before the call, with apartment threading if the program calls `CoInitializeEx`, and says that the call fails when `BIF_NEWDIALOGSTYLE` is passed and COM is initialised as multithreaded. + +In a 64-bit program **ShowBrowseForFolder** ends the program with an access violation whatever **NewDialogStyle** holds, as the warning on its page describes. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.NewDialogStyle ' True + +dlg.NewDialogStyle = False +Debug.Print dlg.NewDialogStyle ' False +``` + +This example shows the old-style dialog. + +```tb check_build +Dim dlg As New cDialog +Dim folder As String + +dlg.NewDialogStyle = False +dlg.DialogTitle = "Choose the backup folder" +folder = dlg.ShowBrowseForFolder() +If Len(folder) > 0 Then + Debug.Print "Chosen: " & folder +End If +``` + +## See Also + +- [ShowBrowseForFolder](ShowBrowseForFolder) method -- shows the Browse For Folder dialog +- [DialogTitle](DialogTitle) property -- the text above the tree of folders +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NoChangeDir.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NoChangeDir.md new file mode 100644 index 00000000..6de31947 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/NoChangeDir.md @@ -0,0 +1,54 @@ +--- +title: NoChangeDir +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/NoChangeDir +has_toc: false +--- + +# NoChangeDir +{: .no_toc } + +Returns or sets whether the Open and Save As dialogs restore the current folder of the program when they close. + +Syntax: *object*.**NoChangeDir** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_NOCHANGEDIR`. The default is **False**. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) use the property. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores it. + +The current folder is the one that **CurDir** returns, and that relative paths are resolved against. With **False**, the current folder is the folder of the chosen file after **ShowOpen** or **ShowSave**. With **True** it stays as it was. The flag works for both dialogs. The Windows documentation of `OFN_NOCHANGEDIR` calls it ineffective for `GetOpenFileName`, but it is effective for **ShowOpen**. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.NoChangeDir ' False + +dlg.NoChangeDir = True +Debug.Print dlg.NoChangeDir ' True +``` + +This example shows the Open dialog and reports whether the current folder is the same afterwards. + +```tb check_build +Dim dlg As New cDialog +Dim before As String + +before = CurDir$ +dlg.NoChangeDir = True +dlg.ShowOpen +Debug.Print CurDir$ = before +``` + +## See Also + +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [InitialDir](InitialDir) property -- the folder in which the dialogs start +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OpenFile.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OpenFile.md new file mode 100644 index 00000000..dae4e4fc --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OpenFile.md @@ -0,0 +1,77 @@ +--- +title: OpenFile +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/OpenFile +has_toc: false +--- + +# OpenFile +{: .no_toc } + +Runs a shell verb, by default `open`, on a file or a program. + +Syntax: *object*.**OpenFile** ( *strFilePath* [, *strOperation* ] ) **As Long** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*strFilePath* +: *required* A **String**, the file to act on or the program to start. Any value that [ShellExecute](https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea) accepts as its file argument is allowed: a document, a program, a folder or a web address. + +*strOperation* +: *optional* A **String**, the verb to run, such as `open`, `edit`, `explore`, `print` or `properties`. The default is `open`. The verbs that exist depend on the type of the file. + +Returns a **Long**, the value that `ShellExecute` returned. A value above 32 means that the call succeeded. A smaller value is an error code. + +| Value | Meaning | +|---|---| +| 0 | The system is out of memory or resources. | +| 2 | The file was not found. | +| 3 | The path was not found. | +| 5 | Access to the file was denied. | +| 8 | There is not enough memory to run the call. | +| 26 | A sharing violation occurred. | +| 31 | No program is associated with the file type for the verb. | + +The Windows documentation of `ShellExecute` lists the other values. + +## Remarks + +**OpenFile** calls `ShellExecuteA`, the ANSI form of `ShellExecute`, with no owner window, no parameters and no working folder, and shows the program's window normally (`SW_SHOWNORMAL`). The method raises no error: the return value is the only report. A path that does not exist returns 2 and shows no message. + +For a program, the verb `open` starts the program. For a document, it starts the program that Windows associates with the document. For a folder, it opens the folder in File Explorer. + +> [!NOTE] +> An empty *strFilePath* is not an error. `OpenFile("")` returns 42, which is above 32 and looks like success, and opens a File Explorer window on the current folder of the program. + +## Example + +This example opens a document and reports an error code. + +```tb check_build +Dim dlg As New cDialog +Dim result As Long + +result = dlg.OpenFile("C:\Reports\summary.pdf") +If result <= 32 Then + Debug.Print "ShellExecute failed with code " & result +End If +``` + +This example edits a text file with the verb `edit`, then shows the folder of the file in Windows Explorer with the verb `explore`. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +path = "C:\Reports\notes.txt" +dlg.OpenFile path, "edit" +dlg.OpenFile dlg.GetFilePath(path), "explore" +``` + +## See Also + +- [SelectAndOpenFile](SelectAndOpenFile) method -- shows the Open dialog and calls **OpenFile** +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [GetFilePath](GetFilePath) method -- the folder part of a path +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OverwritePrompt.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OverwritePrompt.md new file mode 100644 index 00000000..8e18dfcf --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/OverwritePrompt.md @@ -0,0 +1,52 @@ +--- +title: OverwritePrompt +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/OverwritePrompt +has_toc: false +--- + +# OverwritePrompt +{: .no_toc } + +Returns or sets whether the Save As dialog asks for confirmation before it accepts the name of a file that exists. + +Syntax: *object*.**OverwritePrompt** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_OVERWRITEPROMPT`. The default is **True**. + +## Remarks + +[**ShowSave**](ShowSave) uses the property. With **True** and a file that exists, the box *Confirm Save As* asks *name already exists. Do you want to replace it?*, with the buttons *Yes* and *No*. *Yes* returns the path. *No* leaves the Save As dialog open. With **False** there is no box, and the dialog returns the path. + +The dialog only returns the name. The class does not create, replace or check a file. + +[**ShowOpen**](ShowOpen) passes the flag too, and the Windows documentation describes `OFN_OVERWRITEPROMPT` for Save As only. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores the property. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.OverwritePrompt ' True + +dlg.OverwritePrompt = False +Debug.Print dlg.OverwritePrompt ' False +``` + +This example lets the user pick the name of an existing file in the Save As dialog without being asked. + +```tb check_build +Dim dlg As New cDialog +dlg.OverwritePrompt = False +dlg.DialogTitle = "Save the report" +Debug.Print dlg.ShowSave() +``` + +## See Also + +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [PathMustExist](PathMustExist) property -- the check on the folder of the name +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/PathMustExist.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/PathMustExist.md new file mode 100644 index 00000000..6338c378 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/PathMustExist.md @@ -0,0 +1,56 @@ +--- +title: PathMustExist +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/PathMustExist +has_toc: false +--- + +# PathMustExist +{: .no_toc } + +Returns or sets whether the Open and Save As dialogs accept only paths that exist. + +Syntax: *object*.**PathMustExist** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_PATHMUSTEXIST`. The default is **True**. + +## Remarks + +[**ShowOpen**](ShowOpen) and [**ShowSave**](ShowSave) use the property. With **True**, a path that the user types and that does not exist makes Windows show a warning in a message box, and the dialog stays open. [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores the property. + +In an Open dialog, [**FileMustExist**](FileMustExist) **True** sets `OFN_FILEMUSTEXIST`, which Windows documents as implying `OFN_PATHMUSTEXIST`. **ShowOpen** with **FileMustExist** **True** therefore checks the path whatever **PathMustExist** holds. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.PathMustExist ' True + +dlg.PathMustExist = False +Debug.Print dlg.PathMustExist ' False +``` + +This example lets the Save As dialog accept a name in a folder that does not exist yet. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.PathMustExist = False +path = dlg.ShowSave() +If Len(path) > 0 Then + Debug.Print "Chosen: " & path +End If +``` + +## See Also + +- [FileMustExist](FileMustExist) property -- the check on the file of an Open dialog +- [OverwritePrompt](OverwritePrompt) property -- the question about a file that exists +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ReadOnly.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ReadOnly.md new file mode 100644 index 00000000..f0e93023 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ReadOnly.md @@ -0,0 +1,50 @@ +--- +title: ReadOnly +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ReadOnly +has_toc: false +--- + +# ReadOnly +{: .no_toc } + +Returns or sets whether the Open dialog is given the read-only flag `OFN_READONLY`. + +Syntax: *object*.**ReadOnly** [ = *boolean* ] + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +*boolean* +: A **Boolean**. **True** sets `OFN_READONLY`. The default is **False**. + +## Remarks + +Only [**ShowOpen**](ShowOpen) uses the property. [**ShowSave**](ShowSave) never sets `OFN_READONLY`, and [**ShowBrowseForFolder**](ShowBrowseForFolder) ignores the property. + +The Windows documentation describes `OFN_READONLY` as ticking the Read Only check box when the dialog opens. The Open dialog that Windows shows has no such check box, and **ReadOnly** **True** makes no visible difference, whether [**HideReadOnly**](HideReadOnly) is **True** or **False**. The class does not read the flags back from the dialog, so the choice that the user made with the *Open as read-only* menu item (see **HideReadOnly**) is not reported either. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.ReadOnly ' False + +dlg.ReadOnly = True +Debug.Print dlg.ReadOnly ' True +``` + +This example shows the Open dialog with the read-only flag set. + +```tb check_build +Dim dlg As New cDialog +dlg.HideReadOnly = False +dlg.ReadOnly = True +Debug.Print dlg.ShowOpen() +``` + +## See Also + +- [HideReadOnly](HideReadOnly) property -- shows or hides the *Open as read-only* choice +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Reset.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Reset.md new file mode 100644 index 00000000..0e15cea4 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/Reset.md @@ -0,0 +1,44 @@ +--- +title: Reset +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/Reset +has_toc: false +--- + +# Reset +{: .no_toc } + +Restores every property of the **cDialog** to the value it had when the object was created. + +Syntax: *object*.**Reset** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**Reset** empties [**DialogTitle**](DialogTitle), [**InitialDir**](InitialDir), [**DefaultExt**](DefaultExt), [**FileName**](FileName) and [**Filter**](Filter), sets [**MultiSelect**](MultiSelect), [**ReadOnly**](ReadOnly), [**NoChangeDir**](NoChangeDir) and [**CreatePrompt**](CreatePrompt) to **False**, and sets [**OverwritePrompt**](OverwritePrompt), [**PathMustExist**](PathMustExist), [**FileMustExist**](FileMustExist), [**HideReadOnly**](HideReadOnly) and [**NewDialogStyle**](NewDialogStyle) to **True**. These are the starting values listed on the page of the [class](.#defaults). + +A confirmed [**ShowOpen**](ShowOpen) or [**ShowSave**](ShowSave) leaves its path in **FileName**, and a confirmed [**ShowBrowseForFolder**](ShowBrowseForFolder) leaves the folder in **InitialDir**. **Reset** clears both, along with the other properties. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.DialogTitle = "Pick a file" +dlg.AddFilter "All Files (*.*)", "*.*" +dlg.MultiSelect = True +dlg.OverwritePrompt = False + +dlg.Reset +Debug.Print "[" & dlg.DialogTitle & "]" ' [] +Debug.Print Len(dlg.Filter) ' 0 +Debug.Print dlg.MultiSelect ' False +Debug.Print dlg.OverwritePrompt ' True +``` + +## See Also + +- [cDialog](.) class -- overview, with the starting values +- [FileName](FileName) property -- set by a confirmed Open or Save As dialog +- [InitialDir](InitialDir) property -- set by a confirmed Browse For Folder dialog diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SelectAndOpenFile.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SelectAndOpenFile.md new file mode 100644 index 00000000..96c76044 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SelectAndOpenFile.md @@ -0,0 +1,56 @@ +--- +title: SelectAndOpenFile +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/SelectAndOpenFile +has_toc: false +--- + +# SelectAndOpenFile +{: .no_toc } + +Shows the Open dialog and opens the file or files that the user chose. + +Syntax: *object*.**SelectAndOpenFile** ( ) **As Long** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +Returns a **Long**: + +- 0 when the user cancelled the dialog or the dialog failed. +- Otherwise the value that [**OpenFile**](OpenFile) returned for the path. A value above 32 means that the file was opened. + +## Remarks + +**SelectAndOpenFile** calls [**ShowOpen**](ShowOpen) with the current properties of the object, so every option that applies to the Open dialog ([**Filter**](Filter), [**InitialDir**](InitialDir), [**MultiSelect**](MultiSelect) and the others) applies here. It then calls **OpenFile** with the verb `open` for the path. The file is run as **OpenFile** describes: a program starts, and a document opens in its associated program. + +> [!WARNING] +> With [**MultiSelect**](MultiSelect) **True** and several files selected, **ShowOpen** returns only the folder of the files, so **SelectAndOpenFile** passes the folder to **OpenFile**, which opens the folder in File Explorer. None of the selected files is opened, and no error is raised. + +The source also has a branch for an array result, which opens each element and returns 1. **ShowOpen** never returns an array, so the branch never runs. + +## Example + +This example lets the user pick any file and opens it. + +```tb check_build +Dim dlg As New cDialog +Dim result As Long + +dlg.DialogTitle = "Open a document" +dlg.SetDocumentFilters +result = dlg.SelectAndOpenFile() + +If result = 0 Then + Debug.Print "Nothing was opened" +ElseIf result <= 32 Then + Debug.Print "ShellExecute failed with code " & result +End If +``` + +## See Also + +- [OpenFile](OpenFile) method -- opens a file whose path is known +- [ShowOpen](ShowOpen) method -- shows the Open dialog and returns the path +- [MultiSelect](MultiSelect) property -- lets the user select several files +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCodeFilters.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCodeFilters.md new file mode 100644 index 00000000..1007d2d8 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCodeFilters.md @@ -0,0 +1,70 @@ +--- +title: SetCodeFilters +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/SetCodeFilters +has_toc: false +--- + +# SetCodeFilters +{: .no_toc } + +Replaces the [**Filter**](Filter) with pairs for source code files. + +Syntax: *object*.**SetCodeFilters** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**SetCodeFilters** calls [**ClearFilter**](ClearFilter), so any earlier filter is lost, and then [**AddFilter**](AddFilter) for each pair: + +| Description | Pattern | +|---|---| +| `Basic Files (*.bas;*.vba)` | `*.bas;*.vba` | +| `C/C++ Files (*.c;*.cpp;*.h;*.hpp)` | `*.c;*.cpp;*.h;*.hpp` | +| `C# Files (*.cs)` | `*.cs` | +| `Java Files (*.java)` | `*.java` | +| `Python Files (*.py)` | `*.py` | +| `JavaScript Files (*.js)` | `*.js` | +| `HTML Files (*.html;*.htm)` | `*.html;*.htm` | +| `XML Files (*.xml)` | `*.xml` | +| `All Files (*.*)` | `*.*` | + +The descriptions are fixed English text. To show other text, build the filter with [**AddFilter**](AddFilter). The first pair, Basic Files, is the one that the dialog selects when it opens. A call of **AddFilter** after **SetCodeFilters** appends a pair after All Files. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Dim parts() As String +Dim pairCount As Long + +dlg.SetCodeFilters +parts = Split(dlg.Filter, "|") +pairCount = (UBound(parts) + 1) \ 2 +Debug.Print pairCount ' 9 +Debug.Print parts(0) ' Basic Files (*.bas;*.vba) +Debug.Print parts(1) ' *.bas;*.vba +``` + +This example appends a pair for twinBASIC source files to the list. + +```tb check_build check_run +Dim dlg As New cDialog +Dim parts() As String + +dlg.SetCodeFilters +dlg.AddFilter "twinBASIC Files (*.twin)", "*.twin" +parts = Split(dlg.Filter, "|") +Debug.Print parts(UBound(parts) - 1) ' twinBASIC Files (*.twin) +Debug.Print parts(UBound(parts)) ' *.twin +``` + +## See Also + +- [SetCommonFilters](SetCommonFilters) method -- text files +- [SetImageFilters](SetImageFilters) method -- image files +- [SetDocumentFilters](SetDocumentFilters) method -- office documents +- [Filter](Filter) property -- the whole list as one string +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCommonFilters.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCommonFilters.md new file mode 100644 index 00000000..53f44fb7 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetCommonFilters.md @@ -0,0 +1,47 @@ +--- +title: SetCommonFilters +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/SetCommonFilters +has_toc: false +--- + +# SetCommonFilters +{: .no_toc } + +Replaces the [**Filter**](Filter) with the pairs Text Files and All Files. + +Syntax: *object*.**SetCommonFilters** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**SetCommonFilters** calls [**ClearFilter**](ClearFilter), so any earlier filter is lost, and then [**AddFilter**](AddFilter) for each pair: + +| Description | Pattern | +|---|---| +| `Text Files (*.txt)` | `*.txt` | +| `All Files (*.*)` | `*.*` | + +The descriptions are fixed English text. To show other text, build the filter with **AddFilter**. The first pair, Text Files, is the one that the dialog selects when it opens. A call of **AddFilter** after **SetCommonFilters** appends a pair at the end. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +dlg.Filter = "Old|*.old" +dlg.SetCommonFilters +Debug.Print dlg.Filter ' Text Files (*.txt)|*.txt|All Files (*.*)|*.* + +dlg.AddFilter "Logs (*.log)", "*.log" +Debug.Print dlg.Filter ' Text Files (*.txt)|*.txt|All Files (*.*)|*.*|Logs (*.log)|*.log +``` + +## See Also + +- [SetDocumentFilters](SetDocumentFilters) method -- office documents +- [SetImageFilters](SetImageFilters) method -- image files +- [SetCodeFilters](SetCodeFilters) method -- source code files +- [Filter](Filter) property -- the whole list as one string +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetDocumentFilters.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetDocumentFilters.md new file mode 100644 index 00000000..f27f5da1 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetDocumentFilters.md @@ -0,0 +1,54 @@ +--- +title: SetDocumentFilters +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/SetDocumentFilters +has_toc: false +--- + +# SetDocumentFilters +{: .no_toc } + +Replaces the [**Filter**](Filter) with pairs for office documents. + +Syntax: *object*.**SetDocumentFilters** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**SetDocumentFilters** calls [**ClearFilter**](ClearFilter), so any earlier filter is lost, and then [**AddFilter**](AddFilter) for each pair: + +| Description | Pattern | +|---|---| +| `Word Documents (*.doc;*.docx)` | `*.doc;*.docx` | +| `Excel Workbooks (*.xls;*.xlsx)` | `*.xls;*.xlsx` | +| `PowerPoint Presentations (*.ppt;*.pptx)` | `*.ppt;*.pptx` | +| `PDF Documents (*.pdf)` | `*.pdf` | +| `Text Files (*.txt)` | `*.txt` | +| `All Files (*.*)` | `*.*` | + +The descriptions are fixed English text. To show other text, build the filter with [**AddFilter**](AddFilter). The first pair, Word Documents, is the one that the dialog selects when it opens. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Dim parts() As String +Dim pairCount As Long + +dlg.SetDocumentFilters +parts = Split(dlg.Filter, "|") +pairCount = (UBound(parts) + 1) \ 2 +Debug.Print pairCount ' 6 +Debug.Print parts(0) ' Word Documents (*.doc;*.docx) +Debug.Print parts(1) ' *.doc;*.docx +``` + +## See Also + +- [SetCommonFilters](SetCommonFilters) method -- text files +- [SetImageFilters](SetImageFilters) method -- image files +- [SetCodeFilters](SetCodeFilters) method -- source code files +- [Filter](Filter) property -- the whole list as one string +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetImageFilters.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetImageFilters.md new file mode 100644 index 00000000..17fd2f3d --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/SetImageFilters.md @@ -0,0 +1,56 @@ +--- +title: SetImageFilters +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/SetImageFilters +has_toc: false +--- + +# SetImageFilters +{: .no_toc } + +Replaces the [**Filter**](Filter) with pairs for image files. + +Syntax: *object*.**SetImageFilters** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +## Remarks + +**SetImageFilters** calls [**ClearFilter**](ClearFilter), so any earlier filter is lost, and then [**AddFilter**](AddFilter) for each pair: + +| Description | Pattern | +|---|---| +| `Bitmap Files (*.bmp)` | `*.bmp` | +| `JPEG Files (*.jpg;*.jpeg)` | `*.jpg;*.jpeg` | +| `PNG Files (*.png)` | `*.png` | +| `GIF Files (*.gif)` | `*.gif` | +| `All Image Files (*.bmp;*.jpg;*.jpeg;*.png;*.gif)` | `*.bmp;*.jpg;*.jpeg;*.png;*.gif` | +| `All Files (*.*)` | `*.*` | + +The descriptions are fixed English text. To show other text, build the filter with [**AddFilter**](AddFilter). The file-type list of the dialog holds the six descriptions in this order. The first, Bitmap Files, is the one that is selected when the dialog opens, not All Image Files, which is the fifth. + +## Example + +```tb check_build check_run +Dim dlg As New cDialog +Dim parts() As String +Dim pairCount As Long + +dlg.AddFilter "Old", "*.old" +dlg.SetImageFilters + +parts = Split(dlg.Filter, "|") +pairCount = (UBound(parts) + 1) \ 2 +Debug.Print pairCount ' 6 +Debug.Print parts(0) ' Bitmap Files (*.bmp) +Debug.Print parts(1) ' *.bmp +``` + +## See Also + +- [SetCommonFilters](SetCommonFilters) method -- text files +- [SetDocumentFilters](SetDocumentFilters) method -- office documents +- [SetCodeFilters](SetCodeFilters) method -- source code files +- [Filter](Filter) property -- the whole list as one string +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowBrowseForFolder.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowBrowseForFolder.md new file mode 100644 index 00000000..380be5bf --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowBrowseForFolder.md @@ -0,0 +1,67 @@ +--- +title: ShowBrowseForFolder +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ShowBrowseForFolder +has_toc: false +--- + +# ShowBrowseForFolder +{: .no_toc } + +Shows the Browse For Folder dialog and returns the folder that the user chose. + +Syntax: *object*.**ShowBrowseForFolder** ( ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +Returns a **String**, the path of the chosen folder, or an empty string when the user cancelled. + +> [!WARNING] +> **ShowBrowseForFolder** works in 32-bit programs only. In a 64-bit program it ends the program with an access violation (exception 0xC0000005 in KERNELBASE.dll) before any dialog appears, whatever [**NewDialogStyle**](NewDialogStyle) holds, and unsaved data is lost. The class declares the handles and pointers of `BROWSEINFO`, and the item list that `SHBrowseForFolder` returns, as **Long**, which is the wrong size in a 64-bit program. + +## Remarks + +**ShowBrowseForFolder** calls `SHBrowseForFolder` with a `BROWSEINFO` structure, then turns the item that the user chose into a path with `SHGetPathFromIDListA` and frees the item with `CoTaskMemFree`. Two properties apply: + +| Property | What **ShowBrowseForFolder** passes | +|---|---| +| [**DialogTitle**](DialogTitle) | `lpszTitle`, which the dialog shows as a line of text above the tree of folders | +| [**NewDialogStyle**](NewDialogStyle) | `BIF_USENEWUI` (`BIF_NEWDIALOGSTYLE Or BIF_EDITBOX`) when **True** | + +The method always sets `BIF_RETURNONLYFSDIRS`, so only a folder of the file system can be chosen. It passes 0 for `hwndOwner`, so the dialog has no owner window, and it passes no root folder and no callback. No other property is used to set up the dialog. The caption of the dialog is *Browse For Folder* (*Browse for Folder* when **NewDialogStyle** is **False**), and it is not changed by [**DialogTitle**](DialogTitle). + +The dialog does not start in the folder that [**InitialDir**](InitialDir) holds. In the new style it opens with the folder of the user's profile selected, and in the old style with *This PC* selected, with *OK* disabled until the user selects a folder of the file system. + +> [!NOTE] +> When the user confirms a folder, **ShowBrowseForFolder** stores the path in [**InitialDir**](InitialDir). A [**ShowOpen**](ShowOpen) or [**ShowSave**](ShowSave) after it passes that folder to Windows as its starting folder. A path typed into the edit box of the new-style dialog and confirmed is returned and stored in the same way. A cancelled dialog returns an empty string and leaves **InitialDir** as it was. [**FileName**](FileName) is not used or changed. + +The class reads the path with the ANSI function `SHGetPathFromIDListA`, into a buffer of 260 characters. An empty string results when the user cancels, and also when Windows cannot turn the chosen item into a path. The method raises no error. + +## Example + +This example asks for a folder and prints its path. + +```tb check_build +Dim dlg As New cDialog +Dim folder As String + +dlg.DialogTitle = "Choose the backup folder" +folder = dlg.ShowBrowseForFolder() + +If Len(folder) = 0 Then + Debug.Print "Cancelled" +Else + Debug.Print "Chosen: " & folder + Debug.Print "Stored in InitialDir: " & dlg.InitialDir +End If +``` + +## See Also + +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [NewDialogStyle](NewDialogStyle) property -- selects the resizable dialog with an edit box +- [DialogTitle](DialogTitle) property -- the text above the tree of folders +- [InitialDir](InitialDir) property -- receives the chosen folder +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowOpen.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowOpen.md new file mode 100644 index 00000000..9328a4e9 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowOpen.md @@ -0,0 +1,81 @@ +--- +title: ShowOpen +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ShowOpen +has_toc: false +--- + +# ShowOpen +{: .no_toc } + +Shows the Open dialog and returns the file that the user chose. + +Syntax: *object*.**ShowOpen** ( ) **As Variant** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +Returns a **Variant** that always holds a **String**. When the user confirms the dialog, it holds the full path of the chosen file. When the user cancels, it holds an empty string. With [**MultiSelect**](MultiSelect) **True** and several files selected, it holds only a folder, as [below](#several-files) describes. + +## Remarks + +**ShowOpen** calls `GetOpenFileNameW` and takes its options from the properties of the object: + +| Property | What **ShowOpen** passes | +|---|---| +| [**DialogTitle**](DialogTitle) | `lpstrTitle`, the title | +| [**InitialDir**](InitialDir) | `lpstrInitialDir`, the starting folder | +| [**FileName**](FileName) | `lpstrFile`, the text in the File name box when the dialog opens | +| [**Filter**](Filter) | `lpstrFilter`, the file-type list | +| [**DefaultExt**](DefaultExt) | `lpstrDefExt`, the extension added to a name typed without one | +| [**MultiSelect**](MultiSelect) | `OFN_ALLOWMULTISELECT` | +| [**OverwritePrompt**](OverwritePrompt) | `OFN_OVERWRITEPROMPT`, documented for Save As only | +| [**PathMustExist**](PathMustExist) | `OFN_PATHMUSTEXIST` | +| [**FileMustExist**](FileMustExist) | `OFN_FILEMUSTEXIST` | +| [**HideReadOnly**](HideReadOnly) | `OFN_HIDEREADONLY` | +| [**ReadOnly**](ReadOnly) | `OFN_READONLY` | +| [**NoChangeDir**](NoChangeDir) | `OFN_NOCHANGEDIR` | +| [**CreatePrompt**](CreatePrompt) | `OFN_CREATEPROMPT` | + +A flag is set when its property is **True**. The method always sets `OFN_LONGNAMES`, `OFN_EXPLORER` and `OFN_ENABLESIZING`, selects the first pair of the filter, and has no owner window, hook or template. The [class page](.#settings-without-a-property) lists these settings. + +An empty string means that the user cancelled, or that Windows reported a failure, such as a selected path that does not fit into the buffer, and no value tells which of the two happened. The buffer holds 260 characters, or 32,768 when [**MultiSelect**](MultiSelect) is **True** or [**FileName**](FileName) is not empty. Neither case raises an error. + +A confirmed **ShowOpen** also stores the path it returns in [**FileName**](FileName), and leaves [**InitialDir**](InitialDir) as it was. A cancelled dialog changes neither. The next call therefore passes the previous path as the initial file name. + +### Several files + +> [!WARNING] +> With [**MultiSelect**](MultiSelect) **True** and two or more files selected, **ShowOpen** returns a **String** that holds only the folder of the files, never an array, and [**FileName**](FileName) becomes the folder. The names of the files are lost, and no error is raised. One selected file gives its full path. + +Windows returns the selected names in one buffer, and the class cuts that buffer at its first null character, which ends the folder, before it looks for the separators between the names. The code that splits the names and returns an array of full paths therefore never runs. See [**MultiSelect**](MultiSelect) for a way to detect the result. + +## Example + +This example shows the Open dialog for text files and prints the chosen path and name. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.DialogTitle = "Open a text file" +dlg.InitialDir = "C:\Reports" +dlg.SetCommonFilters + +path = dlg.ShowOpen() +If Len(path) = 0 Then + Debug.Print "Cancelled" +Else + Debug.Print path + Debug.Print dlg.GetFileName(path) +End If +``` + +## See Also + +- [ShowSave](ShowSave) method -- shows the Save As dialog +- [ShowBrowseForFolder](ShowBrowseForFolder) method -- shows the Browse For Folder dialog +- [SelectAndOpenFile](SelectAndOpenFile) method -- shows this dialog and opens the file +- [MultiSelect](MultiSelect) property -- lets the user select several files +- [FileName](FileName) property -- holds the path afterwards +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowSave.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowSave.md new file mode 100644 index 00000000..ebf873e4 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/ShowSave.md @@ -0,0 +1,75 @@ +--- +title: ShowSave +parent: cDialog +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ShowSave +has_toc: false +--- + +# ShowSave +{: .no_toc } + +Shows the Save As dialog and returns the file name that the user chose. + +Syntax: *object*.**ShowSave** ( ) **As String** + +*object* +: *required* An object expression that evaluates to a **cDialog**. + +Returns a **String**, the full path that the user confirmed, or an empty string when the user cancelled. + +## Remarks + +**ShowSave** calls `GetSaveFileNameW` and takes its options from the properties of the object: + +| Property | What **ShowSave** passes | +|---|---| +| [**DialogTitle**](DialogTitle) | `lpstrTitle`, the title | +| [**InitialDir**](InitialDir) | `lpstrInitialDir`, the starting folder | +| [**FileName**](FileName) | `lpstrFile`, the text in the File name box when the dialog opens | +| [**Filter**](Filter) | `lpstrFilter`, the file-type list | +| [**DefaultExt**](DefaultExt) | `lpstrDefExt`, the extension added to a name typed without one | +| [**OverwritePrompt**](OverwritePrompt) | `OFN_OVERWRITEPROMPT` | +| [**PathMustExist**](PathMustExist) | `OFN_PATHMUSTEXIST` | +| [**HideReadOnly**](HideReadOnly) | `OFN_HIDEREADONLY` | +| [**NoChangeDir**](NoChangeDir) | `OFN_NOCHANGEDIR` | + +A flag is set when its property is **True**. Unlike [**ShowOpen**](ShowOpen), **ShowSave** never sets `OFN_ALLOWMULTISELECT`, `OFN_FILEMUSTEXIST`, `OFN_READONLY` or `OFN_CREATEPROMPT`, so [**MultiSelect**](MultiSelect), [**FileMustExist**](FileMustExist), [**ReadOnly**](ReadOnly) and [**CreatePrompt**](CreatePrompt) have no effect on it. The method always sets `OFN_LONGNAMES`, `OFN_EXPLORER` and `OFN_ENABLESIZING`, selects the first pair of the filter, and has no owner window, hook or template. The [class page](.#settings-without-a-property) lists these settings. + +When the user confirms the dialog, **ShowSave** stores the path in [**FileName**](FileName) as well as returning it. A cancelled dialog returns an empty string and leaves **FileName** as it was. The dialog returns only a name: it saves nothing, and the class does not create or write the file. + +An empty string also results when Windows reports a failure, such as a path that does not fit into the buffer of 260 characters, and no value tells which of the two happened. Neither case raises an error. + +With [**OverwritePrompt**](OverwritePrompt) **True** and a file that exists, the box *Confirm Save As* asks whether to replace it. *Yes* returns the path, and *No* leaves the dialog open. With **False** there is no box and the path is returned. With [**DefaultExt**](DefaultExt) set, a name typed without an extension gets it. + +> [!NOTE] +> The class pads the buffer with spaces to 260 characters, and the dialog does not show the padding. A [**FileName**](FileName) of 260 characters shows no dialog and returns an empty string at once, as a cancel does. A **FileName** of 261 characters or more makes **ShowSave** raise run-time error 5, *Invalid procedure call or argument*, before any dialog opens. + +## Example + +This example asks for a file name and writes a line to the file. + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.DialogTitle = "Save the report" +dlg.DefaultExt = "txt" +dlg.FileName = "report.txt" +dlg.SetCommonFilters + +path = dlg.ShowSave() +If Len(path) > 0 Then + Open path For Output As #1 + Print #1, "Report" + Close #1 +End If +``` + +## See Also + +- [ShowOpen](ShowOpen) method -- shows the Open dialog +- [ShowBrowseForFolder](ShowBrowseForFolder) method -- shows the Browse For Folder dialog +- [DefaultExt](DefaultExt) property -- the extension added to a name typed without one +- [OverwritePrompt](OverwritePrompt) property -- asks before a name that exists is accepted +- [FileName](FileName) property -- the suggested name and the result +- [cDialog](.) class -- overview diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/index.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/index.md new file mode 100644 index 00000000..f9ca4469 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialog/index.md @@ -0,0 +1,130 @@ +--- +title: cDialog +parent: TBMANLIB_Dialog Package +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/cDialog/ +has_toc: false +--- + +# cDialog class +{: .no_toc } + +Shows the Windows Open, Save As and Browse For Folder dialogs, builds the file-type list of the first two, takes file paths apart, and opens a file in the program that Windows associates with it. + +Properties set the options of a dialog before it is shown. [**ShowOpen**](ShowOpen), [**ShowSave**](ShowSave) and [**ShowBrowseForFolder**](ShowBrowseForFolder) show the dialogs and return the path that the user chose. A cancelled dialog returns an empty string. **ShowOpen** and **ShowSave** also store the path in [**FileName**](FileName). + +```tb check_build +Dim dlg As New cDialog +Dim path As String + +dlg.DialogTitle = "Open a text file" +dlg.SetCommonFilters +path = dlg.ShowOpen() + +If Len(path) > 0 Then + Debug.Print "Chosen: " & path + Debug.Print "Name: " & dlg.GetFileName(path) +End If +``` + +The options are the members and flags of the Win32 structures that the dialogs take: [OPENFILENAME](https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew) for **ShowOpen** (`GetOpenFileNameW`) and **ShowSave** (`GetSaveFileNameW`), and [BROWSEINFO](https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfow) for **ShowBrowseForFolder** (`SHBrowseForFolder`). Each property page names the flag it sets and the dialogs in which it takes effect, and the table [below](#which-dialog-uses-which-property) lists them all. + +> [!NOTE] +> The property names follow VB6's **CommonDialog** control, but **cDialog** is a class with a smaller set of members. It has no **Flags** property (each option is a **Boolean** property of its own), no **FilterIndex** (the first filter is always selected, and the filter that the user chose is not reported), no **CancelError** (a cancelled dialog returns an empty string), no **FileTitle** or **MaxFileSize**, and no owner window. **InitialDir** is the control's **InitDir**. **ShowOpen** returns the file name, as well as storing it in **FileName**, and there is no Color, Font or Print dialog. The [**CommonDialog**](../../VBComDlg/CommonDialog) class of the VBComDlg package has **Flags**, **FilterIndex**, **CancelError**, **FileTitle**, **MaxFileSize** and those dialogs. + +> [!WARNING] +> **ShowBrowseForFolder** works in 32-bit programs only. In a 64-bit program it ends the program with an access violation before any dialog appears, because the class declares the handles, pointers and item list of that dialog as **Long**. + +> [!WARNING] +> With [**MultiSelect**](MultiSelect) **True** and several files selected, **ShowOpen** returns only the folder of the files, as a **String**, and the names of the files are lost. No error is raised. + +## Defaults + +A new **cDialog** starts with these values, and [**Reset**](Reset) restores them. + +| Property | Starting value | +|---|---| +| [**DialogTitle**](DialogTitle), [**InitialDir**](InitialDir), [**DefaultExt**](DefaultExt), [**FileName**](FileName), [**Filter**](Filter) | An empty string | +| [**MultiSelect**](MultiSelect), [**ReadOnly**](ReadOnly), [**NoChangeDir**](NoChangeDir), [**CreatePrompt**](CreatePrompt) | **False** | +| [**OverwritePrompt**](OverwritePrompt), [**PathMustExist**](PathMustExist), [**FileMustExist**](FileMustExist), [**HideReadOnly**](HideReadOnly), [**NewDialogStyle**](NewDialogStyle) | **True** | + +```tb check_build check_run +Dim dlg As New cDialog +Debug.Print dlg.MultiSelect ' False +Debug.Print dlg.FileMustExist ' True +Debug.Print dlg.NewDialogStyle ' True +Debug.Print Len(dlg.Filter) ' 0 +``` + +## Which dialog uses which property + +*yes* means that the class passes the value to Windows for that dialog. A property marked *ignored* is stored and returned, and the dialog does not receive it. **ShowOpen** and **ShowSave** read and write [**FileName**](FileName); **ShowBrowseForFolder** writes [**InitialDir**](InitialDir) when the user chooses a folder, and does not read it. + +| Property | Win32 member or flag | ShowOpen | ShowSave | ShowBrowseForFolder | +|---|---|---|---|---| +| [**DialogTitle**](DialogTitle) | `lpstrTitle`, `lpszTitle` | yes | yes | yes | +| [**InitialDir**](InitialDir) | `lpstrInitialDir` | yes | yes | written, not read | +| [**DefaultExt**](DefaultExt) | `lpstrDefExt` | yes | yes | ignored | +| [**FileName**](FileName) | `lpstrFile` | read and written | read and written | ignored | +| [**Filter**](Filter) | `lpstrFilter` | yes | yes | ignored | +| [**MultiSelect**](MultiSelect) | `OFN_ALLOWMULTISELECT` | yes | ignored | ignored | +| [**OverwritePrompt**](OverwritePrompt) | `OFN_OVERWRITEPROMPT` | yes | yes | ignored | +| [**PathMustExist**](PathMustExist) | `OFN_PATHMUSTEXIST` | yes | yes | ignored | +| [**FileMustExist**](FileMustExist) | `OFN_FILEMUSTEXIST` | yes | ignored | ignored | +| [**HideReadOnly**](HideReadOnly) | `OFN_HIDEREADONLY` | yes | yes | ignored | +| [**ReadOnly**](ReadOnly) | `OFN_READONLY` | yes | ignored | ignored | +| [**NoChangeDir**](NoChangeDir) | `OFN_NOCHANGEDIR` | yes | yes | ignored | +| [**CreatePrompt**](CreatePrompt) | `OFN_CREATEPROMPT` | yes | ignored | ignored | +| [**NewDialogStyle**](NewDialogStyle) | `BIF_USENEWUI` | ignored | ignored | yes | + +Windows documents some of these flags for one kind of dialog only. `OFN_OVERWRITEPROMPT` is documented for Save As, and `OFN_FILEMUSTEXIST` for Open. The class passes **OverwritePrompt** to **ShowOpen** all the same. + +## Settings without a property + +Some settings cannot be changed. + +- **ShowOpen** and **ShowSave** always set `OFN_LONGNAMES`, `OFN_EXPLORER` and `OFN_ENABLESIZING`. Windows documents the effect of `OFN_LONGNAMES` and `OFN_ENABLESIZING` for old-style dialogs, hooks and templates, none of which the class uses. `OFN_EXPLORER` selects the layout of the buffer that a multiple selection returns ([**MultiSelect**](MultiSelect)). +- **ShowOpen** and **ShowSave** always select the first pair of the [**Filter**](Filter) list (`nFilterIndex` is 1) and do not report the pair that the user chose. +- **ShowOpen** and **ShowSave** pass no hook procedure, no custom template and no custom filter. The flags that the dialog returns are not read, so the choice of *Open as read-only* (see [**HideReadOnly**](HideReadOnly)) is not available after the dialog closes. +- **ShowBrowseForFolder** always sets `BIF_RETURNONLYFSDIRS`, so only a folder of the file system can be chosen, and passes no root folder and no callback. +- No dialog has an owner window: the class passes 0 for `hwndOwner` in both structures. + +## Properties + +- [CreatePrompt](CreatePrompt) -- asks before the Open dialog accepts a file that does not exist +- [DefaultExt](DefaultExt) -- the extension that Windows adds to a file name typed without one +- [DialogTitle](DialogTitle) -- the text that the dialogs show as their title +- [FileMustExist](FileMustExist) -- lets the Open dialog accept only the name of an existing file +- [FileName](FileName) -- the file name shown when a dialog opens, and the path that the last Open or Save As dialog returned +- [Filter](Filter) -- the file-type list of the Open and Save As dialogs +- [HideReadOnly](HideReadOnly) -- hides the read-only choice of the Open dialog +- [InitialDir](InitialDir) -- the folder in which the Open and Save As dialogs start +- [MultiSelect](MultiSelect) -- lets the Open dialog select more than one file +- [NewDialogStyle](NewDialogStyle) -- selects the resizable Browse For Folder dialog with an edit box +- [NoChangeDir](NoChangeDir) -- keeps the current folder of the program as it was +- [OverwritePrompt](OverwritePrompt) -- asks before the Save As dialog accepts the name of an existing file +- [PathMustExist](PathMustExist) -- lets the Open and Save As dialogs accept only paths that exist +- [ReadOnly](ReadOnly) -- sets the read-only flag of the Open dialog, which has no visible effect + +## Methods + +- [AddFilter](AddFilter) -- appends a description and a pattern to the filter +- [ClearFilter](ClearFilter) -- empties the filter +- [GetFileExtension](GetFileExtension) -- returns the part of a path from its last period +- [GetFileName](GetFileName) -- returns the part of a path after its last backslash +- [GetFileNameWithoutExt](GetFileNameWithoutExt) -- returns the file name of a path without its extension +- [GetFilePath](GetFilePath) -- returns the part of a path before its last backslash +- [OpenFile](OpenFile) -- runs a shell verb, by default `open`, on a file or a program +- [Reset](Reset) -- restores every property to its starting value +- [SelectAndOpenFile](SelectAndOpenFile) -- shows the Open dialog and opens the chosen file or files +- [SetCodeFilters](SetCodeFilters) -- replaces the filter with one for source code files +- [SetCommonFilters](SetCommonFilters) -- replaces the filter with Text Files and All Files +- [SetDocumentFilters](SetDocumentFilters) -- replaces the filter with one for office documents +- [SetImageFilters](SetImageFilters) -- replaces the filter with one for image files +- [ShowBrowseForFolder](ShowBrowseForFolder) -- shows the Browse For Folder dialog and returns the chosen folder +- [ShowOpen](ShowOpen) -- shows the Open dialog and returns the chosen file +- [ShowSave](ShowSave) -- shows the Save As dialog and returns the chosen file name + +## See Also + +- [TBMANLIB_Dialog](../) package -- overview +- [CommonDialog](../../VBComDlg/CommonDialog) class -- the fuller alternative, with flags, a filter index, cancel errors and the Color, Font and Print dialogs diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/index.md b/docs/Reference/Contributed/TBMANLIB_Dialog/index.md new file mode 100644 index 00000000..4a1d11e0 --- /dev/null +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/index.md @@ -0,0 +1,53 @@ +--- +title: TBMANLIB_Dialog Package +parent: Contributed Packages +has_toc: false +permalink: /tB/Packages/Contributed/TBMANLIB_Dialog/ +indexed_from: 1.0.0.1 +--- + +# TBMANLIB_Dialog Package + +**Author:** woeoio + +A twinBASIC package with one class, **cDialog**, that shows the Windows Open, Save As and Browse For Folder dialogs, builds the file-type filters of the first two, splits a file path into folder, name and extension, and opens a file in the program that Windows associates with it. The property names follow VB6's **CommonDialog** control. The **CommonDialog** class of the [VBComDlg](../VBComDlg/CommonDialog) package is the fuller alternative, with flags, a filter index, cancel errors and the Color, Font and Print dialogs. + +## Classes + +- [cDialog](cDialog/) -- shows the Open, Save As and Browse For Folder dialogs, builds file filters, splits file paths and opens files. + +## Properties (cDialog) + +- [CreatePrompt](cDialog/CreatePrompt) -- asks before the Open dialog accepts a file that does not exist. +- [DefaultExt](cDialog/DefaultExt) -- the extension that Windows adds to a file name typed without one. +- [DialogTitle](cDialog/DialogTitle) -- the text that the dialogs show as their title. +- [FileMustExist](cDialog/FileMustExist) -- lets the Open dialog accept only the name of an existing file. +- [FileName](cDialog/FileName) -- the file name shown when a dialog opens, and the path that the last Open or Save As dialog returned. +- [Filter](cDialog/Filter) -- the file-type list of the Open and Save As dialogs, as pairs of a description and a pattern. +- [HideReadOnly](cDialog/HideReadOnly) -- hides the read-only choice of the Open dialog. +- [InitialDir](cDialog/InitialDir) -- the folder in which the Open and Save As dialogs start. +- [MultiSelect](cDialog/MultiSelect) -- lets the Open dialog select more than one file. +- [NewDialogStyle](cDialog/NewDialogStyle) -- selects the resizable Browse For Folder dialog with an edit box. +- [NoChangeDir](cDialog/NoChangeDir) -- keeps the current folder of the program as it was. +- [OverwritePrompt](cDialog/OverwritePrompt) -- asks before the Save As dialog accepts the name of an existing file. +- [PathMustExist](cDialog/PathMustExist) -- lets the Open and Save As dialogs accept only paths that exist. +- [ReadOnly](cDialog/ReadOnly) -- sets the read-only flag of the Open dialog, which has no visible effect. + +## Methods (cDialog) + +- [AddFilter](cDialog/AddFilter) -- appends a description and a pattern to the filter. +- [ClearFilter](cDialog/ClearFilter) -- empties the filter. +- [GetFileExtension](cDialog/GetFileExtension) -- returns the part of a path from its last period. +- [GetFileName](cDialog/GetFileName) -- returns the part of a path after its last backslash. +- [GetFileNameWithoutExt](cDialog/GetFileNameWithoutExt) -- returns the file name of a path without its extension. +- [GetFilePath](cDialog/GetFilePath) -- returns the part of a path before its last backslash. +- [OpenFile](cDialog/OpenFile) -- runs a shell verb, by default `open`, on a file or a program. +- [Reset](cDialog/Reset) -- restores every property to its starting value. +- [SelectAndOpenFile](cDialog/SelectAndOpenFile) -- shows the Open dialog and opens the chosen file or files. +- [SetCodeFilters](cDialog/SetCodeFilters) -- replaces the filter with one for source code files. +- [SetCommonFilters](cDialog/SetCommonFilters) -- replaces the filter with Text Files and All Files. +- [SetDocumentFilters](cDialog/SetDocumentFilters) -- replaces the filter with one for office documents. +- [SetImageFilters](cDialog/SetImageFilters) -- replaces the filter with one for image files. +- [ShowBrowseForFolder](cDialog/ShowBrowseForFolder) -- shows the Browse For Folder dialog and returns the chosen folder. +- [ShowOpen](cDialog/ShowOpen) -- shows the Open dialog and returns the chosen file. +- [ShowSave](cDialog/ShowSave) -- shows the Save As dialog and returns the chosen file name. diff --git a/docs/_book.yml b/docs/_book.yml index 845e18b8..43c1ad54 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -518,6 +518,13 @@ parts: landing_is_target: true outline_closed: true + - title: TBMANLIB_Dialog Package + subtitle: Open, Save and folder dialogs in a class, with file filters and path helpers + landing_page: /tB/Packages/Contributed/TBMANLIB_Dialog/ + page: /tB/Packages/Contributed/TBMANLIB_Dialog/ + landing_is_target: true + outline_closed: true + - title: TBMANLIB_ListBoxHScroll Package subtitle: A horizontal scroll bar for a ListBox landing_page: /tB/Packages/Contributed/TBMANLIB_ListBoxHScroll/ diff --git a/indexer/snapshots/contributed/TBMANLIB_Dialog/api.json b/indexer/snapshots/contributed/TBMANLIB_Dialog/api.json new file mode 100644 index 00000000..5513a122 --- /dev/null +++ b/indexer/snapshots/contributed/TBMANLIB_Dialog/api.json @@ -0,0 +1,204 @@ +{ + "format": 3, + "package": "TBMANLIB_Dialog", + "project": "TBMANLIB_Dialog", + "version": "1.0.0.1", + "types": [ + { + "name": "cDialog", + "kind": "class", + "vis": "public", + "file": "cDialog.twin", + "signature": "Class cDialog", + "members": [ + {"name":"AddFilter","kind":"sub","vis":"public","signature":"Public Sub AddFilter(ByVal strDescription As String, ByVal strExtension As String)"}, + {"name":"BIF_BROWSEINCLUDEURLS","kind":"const","vis":"private","signature":"Private Const BIF_BROWSEINCLUDEURLS = &H80"}, + {"name":"BIF_DONTGOBELOWDOMAIN","kind":"const","vis":"private","signature":"Private Const BIF_DONTGOBELOWDOMAIN = &H2"}, + {"name":"BIF_EDITBOX","kind":"const","vis":"private","signature":"Private Const BIF_EDITBOX = &H10"}, + {"name":"BIF_NEWDIALOGSTYLE","kind":"const","vis":"private","signature":"Private Const BIF_NEWDIALOGSTYLE = &H40"}, + {"name":"BIF_NONEWFOLDERBUTTON","kind":"const","vis":"private","signature":"Private Const BIF_NONEWFOLDERBUTTON = &H200"}, + {"name":"BIF_NOTRANSLATETARGETS","kind":"const","vis":"private","signature":"Private Const BIF_NOTRANSLATETARGETS = &H400"}, + {"name":"BIF_RETURNFSANCESTORS","kind":"const","vis":"private","signature":"Private Const BIF_RETURNFSANCESTORS = &H8"}, + {"name":"BIF_RETURNONLYFSDIRS","kind":"const","vis":"private","signature":"Private Const BIF_RETURNONLYFSDIRS = &H1"}, + {"name":"BIF_SHAREABLE","kind":"const","vis":"private","signature":"Private Const BIF_SHAREABLE = &H8000"}, + {"name":"BIF_STATUSTEXT","kind":"const","vis":"private","signature":"Private Const BIF_STATUSTEXT = &H4"}, + {"name":"BIF_USENEWUI","kind":"const","vis":"private","signature":"Private Const BIF_USENEWUI = (BIF_NEWDIALOGSTYLE Or BIF_EDITBOX)"}, + {"name":"BIF_VALIDATE","kind":"const","vis":"private","signature":"Private Const BIF_VALIDATE = &H20"}, + {"name":"Class_Initialize","kind":"sub","vis":"private","signature":"Private Sub Class_Initialize()"}, + {"name":"Class_Terminate","kind":"sub","vis":"private","signature":"Private Sub Class_Terminate()"}, + {"name":"ClearFilter","kind":"sub","vis":"public","signature":"Public Sub ClearFilter()"}, + {"name":"CoTaskMemFree","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function CoTaskMemFree Lib \"ole32.dll\" (ByVal hMem As Long) As Long","returnType":"Long"}, + {"name":"CreatePrompt","kind":"property","vis":"public","signature":"Public Property Let CreatePrompt(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"CreatePrompt","kind":"property","vis":"public","signature":"Public Property Get CreatePrompt() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"DefaultExt","kind":"property","vis":"public","signature":"Public Property Let DefaultExt(ByVal vValue As String)","accessor":"Let"}, + {"name":"DefaultExt","kind":"property","vis":"public","signature":"Public Property Get DefaultExt() As String","returnType":"String","accessor":"Get"}, + {"name":"DialogTitle","kind":"property","vis":"public","signature":"Public Property Let DialogTitle(ByVal vValue As String)","accessor":"Let"}, + {"name":"DialogTitle","kind":"property","vis":"public","signature":"Public Property Get DialogTitle() As String","returnType":"String","accessor":"Get"}, + {"name":"FileMustExist","kind":"property","vis":"public","signature":"Public Property Let FileMustExist(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"FileMustExist","kind":"property","vis":"public","signature":"Public Property Get FileMustExist() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"FileName","kind":"property","vis":"public","signature":"Public Property Let FileName(ByVal vValue As String)","accessor":"Let"}, + {"name":"FileName","kind":"property","vis":"public","signature":"Public Property Get FileName() As String","returnType":"String","accessor":"Get"}, + {"name":"Filter","kind":"property","vis":"public","signature":"Public Property Let Filter(ByVal vValue As String)","accessor":"Let"}, + {"name":"Filter","kind":"property","vis":"public","signature":"Public Property Get Filter() As String","returnType":"String","accessor":"Get"}, + {"name":"GMEM_FIXED","kind":"const","vis":"private","signature":"Private Const GMEM_FIXED = &H0"}, + {"name":"GMEM_MOVEABLE","kind":"const","vis":"private","signature":"Private Const GMEM_MOVEABLE = &H2"}, + {"name":"GMEM_ZEROINIT","kind":"const","vis":"private","signature":"Private Const GMEM_ZEROINIT = &H40"}, + {"name":"GPTR","kind":"const","vis":"private","signature":"Private Const GPTR = (GMEM_FIXED Or GMEM_ZEROINIT)"}, + {"name":"GetFileExtension","kind":"function","vis":"public","signature":"Public Function GetFileExtension(ByVal strFullPath As String) As String","returnType":"String"}, + {"name":"GetFileName","kind":"function","vis":"public","signature":"Public Function GetFileName(ByVal strFullPath As String) As String","returnType":"String"}, + {"name":"GetFileNameWithoutExt","kind":"function","vis":"public","signature":"Public Function GetFileNameWithoutExt(ByVal strFullPath As String) As String","returnType":"String"}, + {"name":"GetFilePath","kind":"function","vis":"public","signature":"Public Function GetFilePath(ByVal strFullPath As String) As String","returnType":"String"}, + {"name":"GetOpenFileName","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GetOpenFileName Lib \"comdlg32.dll\" Alias \"GetOpenFileNameW\" (pOpenfilename As OPENFILENAME) As Long","returnType":"Long"}, + {"name":"GetSaveFileName","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GetSaveFileName Lib \"comdlg32.dll\" Alias \"GetSaveFileNameW\" (pOpenfilename As OPENFILENAME) As Long","returnType":"Long"}, + {"name":"GlobalAlloc","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GlobalAlloc Lib \"kernel32\" (ByVal wFlags As Long, ByVal dwBytes As Long) As Long","returnType":"Long"}, + {"name":"GlobalLock","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GlobalLock Lib \"kernel32\" (ByVal hMem As Long) As Long","returnType":"Long"}, + {"name":"GlobalUnlock","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GlobalUnlock Lib \"kernel32\" (ByVal hMem As Long) As Long","returnType":"Long"}, + {"name":"HideReadOnly","kind":"property","vis":"public","signature":"Public Property Let HideReadOnly(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"HideReadOnly","kind":"property","vis":"public","signature":"Public Property Get HideReadOnly() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"InitialDir","kind":"property","vis":"public","signature":"Public Property Let InitialDir(ByVal vValue As String)","accessor":"Let"}, + {"name":"InitialDir","kind":"property","vis":"public","signature":"Public Property Get InitialDir() As String","returnType":"String","accessor":"Get"}, + {"name":"LocalFree","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function LocalFree Lib \"kernel32\" (ByVal hMem As Long) As Long","returnType":"Long"}, + {"name":"MAX_PATH","kind":"const","vis":"private","signature":"Private Const MAX_PATH = 260"}, + {"name":"MultiSelect","kind":"property","vis":"public","signature":"Public Property Let MultiSelect(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"MultiSelect","kind":"property","vis":"public","signature":"Public Property Get MultiSelect() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"NewDialogStyle","kind":"property","vis":"public","signature":"Public Property Let NewDialogStyle(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"NewDialogStyle","kind":"property","vis":"public","signature":"Public Property Get NewDialogStyle() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"NoChangeDir","kind":"property","vis":"public","signature":"Public Property Let NoChangeDir(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"NoChangeDir","kind":"property","vis":"public","signature":"Public Property Get NoChangeDir() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"OFN_ALLOWMULTISELECT","kind":"const","vis":"private","signature":"Private Const OFN_ALLOWMULTISELECT = &H200"}, + {"name":"OFN_CREATEPROMPT","kind":"const","vis":"private","signature":"Private Const OFN_CREATEPROMPT = &H2000"}, + {"name":"OFN_ENABLEHOOK","kind":"const","vis":"private","signature":"Private Const OFN_ENABLEHOOK = &H20"}, + {"name":"OFN_ENABLEINCLUDENOTIFY","kind":"const","vis":"private","signature":"Private Const OFN_ENABLEINCLUDENOTIFY = &H400000"}, + {"name":"OFN_ENABLESIZING","kind":"const","vis":"private","signature":"Private Const OFN_ENABLESIZING = &H800000"}, + {"name":"OFN_ENABLETEMPLATE","kind":"const","vis":"private","signature":"Private Const OFN_ENABLETEMPLATE = &H40"}, + {"name":"OFN_ENABLETEMPLATEHANDLE","kind":"const","vis":"private","signature":"Private Const OFN_ENABLETEMPLATEHANDLE = &H80"}, + {"name":"OFN_EXPLORER","kind":"const","vis":"private","signature":"Private Const OFN_EXPLORER = &H80000"}, + {"name":"OFN_EXTENSIONDIFFERENT","kind":"const","vis":"private","signature":"Private Const OFN_EXTENSIONDIFFERENT = &H400"}, + {"name":"OFN_FILEMUSTEXIST","kind":"const","vis":"private","signature":"Private Const OFN_FILEMUSTEXIST = &H1000"}, + {"name":"OFN_HIDEREADONLY","kind":"const","vis":"private","signature":"Private Const OFN_HIDEREADONLY = &H4"}, + {"name":"OFN_LONGNAMES","kind":"const","vis":"private","signature":"Private Const OFN_LONGNAMES = &H200000"}, + {"name":"OFN_NOCHANGEDIR","kind":"const","vis":"private","signature":"Private Const OFN_NOCHANGEDIR = &H8"}, + {"name":"OFN_NODEREFERENCELINKS","kind":"const","vis":"private","signature":"Private Const OFN_NODEREFERENCELINKS = &H100000"}, + {"name":"OFN_NOLONGNAMES","kind":"const","vis":"private","signature":"Private Const OFN_NOLONGNAMES = &H40000"}, + {"name":"OFN_NONETWORKBUTTON","kind":"const","vis":"private","signature":"Private Const OFN_NONETWORKBUTTON = &H20000"}, + {"name":"OFN_NOREADONLYRETURN","kind":"const","vis":"private","signature":"Private Const OFN_NOREADONLYRETURN = &H8000"}, + {"name":"OFN_NOTESTFILECREATE","kind":"const","vis":"private","signature":"Private Const OFN_NOTESTFILECREATE = &H10000"}, + {"name":"OFN_NOVALIDATE","kind":"const","vis":"private","signature":"Private Const OFN_NOVALIDATE = &H100"}, + {"name":"OFN_OVERWRITEPROMPT","kind":"const","vis":"private","signature":"Private Const OFN_OVERWRITEPROMPT = &H2"}, + {"name":"OFN_PATHMUSTEXIST","kind":"const","vis":"private","signature":"Private Const OFN_PATHMUSTEXIST = &H800"}, + {"name":"OFN_READONLY","kind":"const","vis":"private","signature":"Private Const OFN_READONLY = &H1"}, + {"name":"OFN_SHAREAWARE","kind":"const","vis":"private","signature":"Private Const OFN_SHAREAWARE = &H4000"}, + {"name":"OFN_SHOWHELP","kind":"const","vis":"private","signature":"Private Const OFN_SHOWHELP = &H10"}, + {"name":"OpenFile","kind":"function","vis":"public","signature":"Public Function OpenFile(ByVal strFilePath As String, Optional ByVal strOperation As String = \"open\") As Long","returnType":"Long"}, + {"name":"OverwritePrompt","kind":"property","vis":"public","signature":"Public Property Let OverwritePrompt(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"OverwritePrompt","kind":"property","vis":"public","signature":"Public Property Get OverwritePrompt() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"PathMustExist","kind":"property","vis":"public","signature":"Public Property Let PathMustExist(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"PathMustExist","kind":"property","vis":"public","signature":"Public Property Get PathMustExist() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"ReadOnly","kind":"property","vis":"public","signature":"Public Property Let ReadOnly(ByVal vValue As Boolean)","accessor":"Let"}, + {"name":"ReadOnly","kind":"property","vis":"public","signature":"Public Property Get ReadOnly() As Boolean","returnType":"Boolean","accessor":"Get"}, + {"name":"Reset","kind":"sub","vis":"public","signature":"Public Sub Reset()"}, + {"name":"SHBrowseForFolder","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function SHBrowseForFolder Lib \"shell32.dll\" (lpbi As BROWSEINFO) As Long","returnType":"Long"}, + {"name":"SHGetPathFromIDList","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function SHGetPathFromIDList Lib \"shell32.dll\" Alias \"SHGetPathFromIDListA\" (ByVal pidl As Long, ByVal pszPath As String) As Long","returnType":"Long"}, + {"name":"SelectAndOpenFile","kind":"function","vis":"public","signature":"Public Function SelectAndOpenFile() As Long","returnType":"Long"}, + {"name":"SetCodeFilters","kind":"sub","vis":"public","signature":"Public Sub SetCodeFilters()"}, + {"name":"SetCommonFilters","kind":"sub","vis":"public","signature":"Public Sub SetCommonFilters()"}, + {"name":"SetDocumentFilters","kind":"sub","vis":"public","signature":"Public Sub SetDocumentFilters()"}, + {"name":"SetImageFilters","kind":"sub","vis":"public","signature":"Public Sub SetImageFilters()"}, + {"name":"ShellExecute","kind":"function","vis":"private","declare":true,"returnType":"Long","signatures":[{"cond":"VBA7","signature":"Private Declare PtrSafe Function ShellExecute Lib \"shell32.dll\" Alias \"ShellExecuteA\" (ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long"},{"cond":"Not (VBA7)","signature":"Private Declare Function ShellExecute Lib \"shell32.dll\" Alias \"ShellExecuteA\" (ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long"}]}, + {"name":"ShowBrowseForFolder","kind":"function","vis":"public","signature":"Public Function ShowBrowseForFolder() As String","returnType":"String"}, + {"name":"ShowOpen","kind":"function","vis":"public","signature":"Public Function ShowOpen() As Variant","returnType":"Variant"}, + {"name":"ShowSave","kind":"function","vis":"public","signature":"Public Function ShowSave() As String","returnType":"String"}, + {"name":"lstrcpy","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function lstrcpy Lib \"kernel32\" Alias \"lstrcpyA\" (ByVal lpString1 As Long, ByVal lpString2 As Long) As Long","returnType":"Long"}, + {"name":"lstrlen","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function lstrlen Lib \"kernel32\" Alias \"lstrlenA\" (ByVal lpString As Long) As Long","returnType":"Long"}, + {"name":"mData","kind":"field","vis":"private","signature":"Private mData As cDialogData"} + ] + }, + { + "name": "BROWSEINFO", + "kind": "type", + "in": "cDialog", + "vis": "private", + "public": false, + "file": "cDialog.twin", + "signature": "Private Type BROWSEINFO", + "fields": [ + {"name":"hwndOwner","vis":"public","signature":"hwndOwner As Long"}, + {"name":"pidlRoot","vis":"public","signature":"pidlRoot As Long"}, + {"name":"pszDisplayName","vis":"public","signature":"pszDisplayName As String"}, + {"name":"lpszTitle","vis":"public","signature":"lpszTitle As String"}, + {"name":"ulFlags","vis":"public","signature":"ulFlags As Long"}, + {"name":"lpfnCallback","vis":"public","signature":"lpfnCallback As Long"}, + {"name":"lParam","vis":"public","signature":"lParam As Long"}, + {"name":"iImage","vis":"public","signature":"iImage As Long"} + ], + "members": [] + }, + { + "name": "OPENFILENAME", + "kind": "type", + "in": "cDialog", + "vis": "private", + "public": false, + "file": "cDialog.twin", + "signature": "Private Type OPENFILENAME", + "fields": [ + {"name":"lStructSize","vis":"public","signature":"lStructSize As Long"}, + {"name":"hwndOwner","vis":"public","signature":"hwndOwner As LongPtr"}, + {"name":"hInstance","vis":"public","signature":"hInstance As LongPtr"}, + {"name":"lpstrFilter","vis":"public","signature":"lpstrFilter As LongPtr"}, + {"name":"lpstrCustomFilter","vis":"public","signature":"lpstrCustomFilter As LongPtr"}, + {"name":"nMaxCustFilter","vis":"public","signature":"nMaxCustFilter As Long"}, + {"name":"nFilterIndex","vis":"public","signature":"nFilterIndex As Long"}, + {"name":"lpstrFile","vis":"public","signature":"lpstrFile As LongPtr"}, + {"name":"nMaxFile","vis":"public","signature":"nMaxFile As Long"}, + {"name":"lpstrFileTitle","vis":"public","signature":"lpstrFileTitle As LongPtr"}, + {"name":"nMaxFileTitle","vis":"public","signature":"nMaxFileTitle As Long"}, + {"name":"lpstrInitialDir","vis":"public","signature":"lpstrInitialDir As LongPtr"}, + {"name":"lpstrTitle","vis":"public","signature":"lpstrTitle As LongPtr"}, + {"name":"flags","vis":"public","signature":"flags As Long"}, + {"name":"nFileOffset","vis":"public","signature":"nFileOffset As Integer"}, + {"name":"nFileExtension","vis":"public","signature":"nFileExtension As Integer"}, + {"name":"lpstrDefExt","vis":"public","signature":"lpstrDefExt As LongPtr"}, + {"name":"lCustData","vis":"public","signature":"lCustData As LongPtr"}, + {"name":"lpfnHook","vis":"public","signature":"lpfnHook As LongPtr"}, + {"name":"lpTemplateName","vis":"public","signature":"lpTemplateName As LongPtr"}, + {"name":"pvReserved","vis":"public","signature":"pvReserved As Long"}, + {"name":"dwReserved","vis":"public","signature":"dwReserved As Long"}, + {"name":"FlagsEx","vis":"public","signature":"FlagsEx As Long"} + ], + "members": [] + }, + { + "name": "cDialogData", + "kind": "type", + "in": "cDialog", + "vis": "private", + "public": false, + "file": "cDialog.twin", + "signature": "Private Type cDialogData", + "fields": [ + {"name":"mstrDialogTitle","vis":"public","signature":"mstrDialogTitle As String"}, + {"name":"mstrInitialDir","vis":"public","signature":"mstrInitialDir As String"}, + {"name":"mstrDefaultExt","vis":"public","signature":"mstrDefaultExt As String"}, + {"name":"mstrFileName","vis":"public","signature":"mstrFileName As String"}, + {"name":"mstrFilter","vis":"public","signature":"mstrFilter As String"}, + {"name":"mblnMultiSelect","vis":"public","signature":"mblnMultiSelect As Boolean"}, + {"name":"mblnOverwritePrompt","vis":"public","signature":"mblnOverwritePrompt As Boolean"}, + {"name":"mblnPathMustExist","vis":"public","signature":"mblnPathMustExist As Boolean"}, + {"name":"mblnFileMustExist","vis":"public","signature":"mblnFileMustExist As Boolean"}, + {"name":"mblnHideReadOnly","vis":"public","signature":"mblnHideReadOnly As Boolean"}, + {"name":"mblnReadOnly","vis":"public","signature":"mblnReadOnly As Boolean"}, + {"name":"mblnNoChangeDir","vis":"public","signature":"mblnNoChangeDir As Boolean"}, + {"name":"mblnCreatePrompt","vis":"public","signature":"mblnCreatePrompt As Boolean"}, + {"name":"mblnNoReadOnlyReturn","vis":"public","signature":"mblnNoReadOnlyReturn As Boolean"}, + {"name":"mblnLongNames","vis":"public","signature":"mblnLongNames As Boolean"}, + {"name":"mblnExplorer","vis":"public","signature":"mblnExplorer As Boolean"}, + {"name":"mblnEnableSizing","vis":"public","signature":"mblnEnableSizing As Boolean"}, + {"name":"mblnNewDialogStyle","vis":"public","signature":"mblnNewDialogStyle As Boolean"} + ], + "members": [] + } + ], + "aliases": [], + "problems": [] +} diff --git a/test/README.md b/test/README.md index 9cbdff3e..96720a62 100644 --- a/test/README.md +++ b/test/README.md @@ -14,6 +14,9 @@ is input for three tools: - `ide/` for [`scripts/ide_test.mjs`](../scripts/ide_test.mjs), which `ide-test.bat` runs: the scenarios that operate the IDE itself. It is the add-in runner over its own `lanes.mjs`, and its scenario files use `scenario` from `addin/scenario.mjs`. +- `package-probes/` for a person documenting a contributed package: a probe of what a + package does at run time that only a built program can show, run by hand and outside every + gate, with a README of its own. `TBMANLIB_Dialog/` operates the package's dialogs. If a fixture case has just gone red, start at [The invariant, and how it breaks](#the-invariant-and-how-it-breaks) --- the commit that broke it need not have touched diff --git a/test/example-projects/contributed-TBMANLIB_Dialog/Settings b/test/example-projects/contributed-TBMANLIB_Dialog/Settings new file mode 100644 index 00000000..3a0c91fa --- /dev/null +++ b/test/example-projects/contributed-TBMANLIB_Dialog/Settings @@ -0,0 +1,74 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Documentation samples", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Template for scripts/check_examples.mjs. project.id, project.name and project.buildPath are rewritten in the staged copy, one per batch.", + "project.exportPathIsV2": true, + "project.id": "{7B247400-0000-4000-9000-7B2474000000}", + "project.name": "DocSamples", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "autoCtlExt": true, + "id": "{39D62B5D-D7E2-4EF7-8589-BEE086A4AD14}", + "licence": "", + "name": "[IMPORTED] TBMAN: A file dialog has open/save functions, and a folder dialog has an input box.", + "path32": "/Packages/TBMANLIB_Dialog", + "path64": "/Packages/TBMANLIB_Dialog", + "symbolId": "TBMANLIB_Dialog", + "versionBuild": "1", + "versionMajor": "1", + "versionMinor": "0", + "versionRevision": "0" + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/package-probes/TBMANLIB_Dialog/README.md b/test/package-probes/TBMANLIB_Dialog/README.md new file mode 100644 index 00000000..a7567e0e --- /dev/null +++ b/test/package-probes/TBMANLIB_Dialog/README.md @@ -0,0 +1,49 @@ +# TBMANLIB_Dialog probe + +What the contributed package TBMANLIB_Dialog's **cDialog** does at run time: the Open, Save As +and Browse For Folder dialogs, opened in a built exe on a private desktop and operated from +inside the exe. The pages under `docs/Reference/Contributed/TBMANLIB_Dialog/` state what this +probe printed, on package 1.0.0.1 and BETA 1005, in 32-bit and 64-bit builds. Run it again when +the section's `indexed_from` moves to a new version of the package. + +It is outside every gate and CI: it needs Windows, a twinBASIC install (the newest +`twinBASIC_IDE_BETA_` on the Desktop, as `tbrun` finds it) and the package in the indexer's +store (`node indexer/twin-index.mjs sync`). + +## Running it + +```sh +node test/package-probes/TBMANLIB_Dialog/probe.mjs 13,1,2,3 4,5 6 6d,7 8,9,9e 11,12,12b,12c +node test/package-probes/TBMANLIB_Dialog/probe.mjs --arch win64 --port 9722 13,1,2 3 4 5 6,6d 7,8 9 9e 11,12,12b,12c +``` + +In a 64-bit build, scenarios 3, 4, 9 and 9e each end their run when they reach +**ShowBrowseForFolder**, so each has a run of its own (3 and 4 print their Open and Save As +parts first). + +Each argument is one run of the exe; the scenario ids are listed at the top of +[`src/Probe.twin`](src/Probe.twin), each the number of a question it answers. A build takes +about ten seconds and a run under 25 s; both commands take a minute or two. Exit 1 means a run +crashed or timed out, which is the expected answer for those four in a 64-bit build. + +- [`probe.mjs`](probe.mjs) writes the project (the `contributed-TBMANLIB_Dialog` sample template's + Settings, the package from the store, `src/`), builds it with `tbrun` and runs the exe once for + each list of ids. +- [`run-exe.mjs`](run-exe.mjs) runs a built exe on a private desktop. `tbrun --exe` would do that + too, but it presses OK on every dialog the exe opens within 250 ms, which races the probe's + own automation and answers its Yes/No boxes. With `PROBE_SHOTS=1 SHOTS=` it photographs + each dialog and menu; that is how the text of the task-dialog message boxes, which expose no + readable text, was read. +- [`src/Auto.twin`](src/Auto.twin) operates a dialog from inside the exe: a thread timer, whose + messages the dialog's modal loop dispatches, finds the dialog, reads its title, controls and + file-type list, types into it and presses its buttons. It cancels a dialog after 3.6 s and ends + the process after 5.4 s, so nothing waits on a dialog it cannot operate. + +## Rules + +- Never call **OpenFile** or **SelectAndOpenFile** on a path that exists, or on `""`: it starts + a program. `OpenFile("")` opens a File Explorer window, and the COM-started `explorer.exe` + behind it outlives the exe, with its window on the private desktop where nobody can close + it; it has to be ended by its pid. +- Never run the exe on the user's desktop, or a dialog in the IDE: the probe's `[RunAfterBuild]` + Sub only prints. diff --git a/test/package-probes/TBMANLIB_Dialog/probe.mjs b/test/package-probes/TBMANLIB_Dialog/probe.mjs new file mode 100644 index 00000000..f0fc1f4a --- /dev/null +++ b/test/package-probes/TBMANLIB_Dialog/probe.mjs @@ -0,0 +1,127 @@ +// Builds the TBMANLIB_Dialog probe once, then runs the built exe once for each list of scenario +// ids, on a private desktop. What the package's dialogs do at run time, for the pages under +// docs/Reference/Contributed/TBMANLIB_Dialog/: see README.md beside this file. +// +// node test/package-probes/TBMANLIB_Dialog/probe.mjs [--arch win32|win64] [--port N] [ ...] +// +// 1. The source tree is written to %TEMP%\dialog-probe\tree-: the contributed template's +// Settings (test/example-projects/contributed-TBMANLIB_Dialog), the package embedded under +// Packages/TBMANLIB_Dialog as check_examples embeds it, from the indexer's store +// (indexer/.packages/contributed/TBMANLIB_Dialog, written by `twin-index sync`), and src/. +// 2. tbrun builds it (its [RunAfterBuild] Sub only prints; no dialog opens in the IDE), with +// --timeout 35. Not --exe: run-exe.mjs says why. +// 3. run-exe.mjs runs the exe tbrun built once per , each run ended after 25 s. +import { spawnSync } from "node:child_process"; +import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { + choiceOption, + exitOnCrash, + numberOption, + parseCli, + printHelpAndExit, + withUsageError, +} from "../../../lib/cli.mjs"; + +const USAGE = `usage: node test/package-probes/TBMANLIB_Dialog/probe.mjs [--arch win32|win64] [--port N] [ ...] [-h, --help] + +Builds the TBMANLIB_Dialog probe (src/) with tbrun, then runs the exe on a private desktop once +for each , a comma-separated list of the scenario ids listed at the top of src/Probe.twin. + + --arch A win32 (default) or win64 + --port N the IDE's DevTools port (default 9720); it also keys the work folders + -h, --help print this text and exit + +Exit codes: + 0 the probe was built, and every run of the exe ended normally + 1 a run of the exe crashed, exited with another code, or was still running at its timeout + 2 the command line was refused, the package is not in the indexer's store, the build failed, + or the probe could not run`; + +exitOnCrash(); +const usageError = { format: (err) => `${err.message}\n${USAGE}` }; +const { values, positionals } = withUsageError( + () => + parseCli(process.argv.slice(2), { + options: { + arch: { type: "string", default: "win32" }, + port: { type: "string", default: "9720" }, + help: { type: "boolean", short: "h" }, + }, + positionals: { min: 1 }, + stopAt: ["help"], + }), + usageError, +); +if (values.help) printHelpAndExit(USAGE); +const { arch, port } = withUsageError( + () => ({ + arch: choiceOption(values.arch, { option: "--arch", choices: ["win32", "win64"] }), + port: numberOption(values.port, { option: "--port", integer: true, min: 1024, max: 65535 }), + }), + usageError, +); + +const here = import.meta.dirname; +const repo = path.resolve(here, "..", "..", ".."); +const pkg = path.join(repo, "indexer", ".packages", "contributed", "TBMANLIB_Dialog"); +if (!existsSync(path.join(pkg, "sources"))) { + console.error(`probe: no package sources in ${pkg}; run \`node indexer/twin-index.mjs sync\` first`); + process.exit(2); +} + +// 1. The source tree, with CRLF as the IDE writes it. +const tree = path.join(tmpdir(), "dialog-probe", `tree-${port}`); +rmSync(tree, { recursive: true, force: true }); +mkdirSync(path.join(tree, "Sources"), { recursive: true }); +cpSync( + path.join(repo, "test", "example-projects", "contributed-TBMANLIB_Dialog", "Settings"), + path.join(tree, "Settings"), +); +const embedded = path.join(tree, "Packages", "TBMANLIB_Dialog"); +mkdirSync(path.join(embedded, "Sources"), { recursive: true }); +cpSync(path.join(pkg, "Settings"), path.join(embedded, "Settings")); +for (const f of readdirSync(path.join(pkg, "sources"))) + cpSync(path.join(pkg, "sources", f), path.join(embedded, "Sources", f)); +for (const f of readdirSync(path.join(here, "src"))) { + const text = readFileSync(path.join(here, "src", f), "utf8").replace(/\r?\n/g, "\r\n"); + writeFileSync(path.join(tree, "Sources", f), text, "utf8"); +} + +// 2. The build. +const tbrun = path.join(repo, "scripts", "tbrun.mjs"); +const build = spawnSync(process.execPath, [tbrun, tree, "--arch", arch, "--port", String(port), "--timeout", "35"], { + cwd: repo, + encoding: "utf8", +}); +if (build.status !== 0) { + console.error(`probe: the build failed (tbrun exit ${build.status})`); + console.error(`${build.stdout ?? ""}${build.stderr ?? ""}`.trim().split("\n").slice(-40).join("\n")); + process.exit(2); +} +const outDir = path.join(tmpdir(), "tbrun", String(port), "out"); +const exe = existsSync(outDir) && readdirSync(outDir).find((f) => f.toLowerCase().endsWith(`_${arch}.exe`)); +if (!exe) { + console.error(`probe: tbrun wrote no ${arch} exe to ${outDir}`); + process.exit(2); +} + +// 3. The runs. +let failed = false; +for (const ids of positionals) { + console.log(`######## ${arch} run: ${ids}`); + const run = spawnSync( + process.execPath, + [path.join(here, "run-exe.mjs"), path.join(outDir, exe), ids, "--timeout", "25", "--desktop", `dlgprobe-${port}`], + { cwd: repo, stdio: "inherit" }, + ); + if (run.status === 2 || run.status === null) { + console.error(`probe: run-exe failed (exit ${run.status})`); + process.exit(2); + } + if (run.status !== 0) failed = true; +} +rmSync(tree, { recursive: true, force: true }); +rmSync(path.join(tmpdir(), "tbrun", String(port)), { recursive: true, force: true }); +process.exitCode = failed ? 1 : 0; diff --git a/test/package-probes/TBMANLIB_Dialog/run-exe.mjs b/test/package-probes/TBMANLIB_Dialog/run-exe.mjs new file mode 100644 index 00000000..f8ea280e --- /dev/null +++ b/test/package-probes/TBMANLIB_Dialog/run-exe.mjs @@ -0,0 +1,143 @@ +// Runs a built probe exe on a private desktop, WITHOUT tbrun --exe's dialog closer. +// +// node test/package-probes/TBMANLIB_Dialog/run-exe.mjs [--timeout S] [--desktop NAME] +// +// tbrun --exe closes every dialog the exe opens within 250 ms by pressing its OK button, which +// would race the probe's own automation (src/Auto.twin) and answer the Yes/No boxes itself. This +// runner starts the exe the same way tbrun does (launchOnDesktop: a private desktop, in a +// kill-on-close job, so nothing appears on the user's desktop) but with no `dialogs` option, and +// reads what the exe wrote with TbRun.Out from the file TBRUN_OUT names. A run that is still going +// after the timeout is ended by its pid (the job takes its children with it), and the windows it +// left on the desktop are listed. +// +// It prints the exe's output, then "--- exit code", and "--- fault recorded" when the event +// log has one for the exe (win-fault.mjs, as tbrun --exe reads it). The probe's work folder, +// %TEMP%\dlgprobe, is removed afterwards. +// +// Environment: PROBE_SHOTS=1 makes the exe wait several seconds before it acts on each window; +// SHOTS= then photographs every new dialog or menu window (PrintWindow, through +// captureWindow) into it. +import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../lib/cli.mjs"; +import { captureWindow, desktopWindows, killTree, launchOnDesktop } from "../../../scripts/lib/tb-ide.mjs"; +import { faultText, recordedFault } from "../../../scripts/lib/win-fault.mjs"; + +const USAGE = `usage: node test/package-probes/TBMANLIB_Dialog/run-exe.mjs [--timeout S] [--desktop NAME] [-h, --help] + +Runs the TBMANLIB_Dialog probe exe on a private desktop, with the scenarios names +(comma-separated, as listed at the top of src/Probe.twin), and prints what they report. + + --timeout S end the exe after S seconds (default 25) + --desktop NAME the private desktop's name (default dlgprobe-) + -h, --help print this text and exit + +Exit codes: + 0 the exe ran to its end and exited 0 + 1 the exe exited with another code (a crash included) or was still running at the timeout + 2 the command line was refused, or the runner failed`; + +exitOnCrash(); +const usageError = { format: (err) => `${err.message}\n${USAGE}` }; +const { values, positionals } = withUsageError( + () => + parseCli(process.argv.slice(2), { + options: { + timeout: { type: "string", default: "25" }, + desktop: { type: "string" }, + help: { type: "boolean", short: "h" }, + }, + positionals: 2, + stopAt: ["help"], + }), + usageError, +); +if (values.help) printHelpAndExit(USAGE); +const timeoutS = withUsageError(() => numberOption(values.timeout, { option: "--timeout", above: 0 }), usageError); + +const [exe, probe] = positionals; +const timeoutMs = timeoutS * 1000; +const desktop = values.desktop || `dlgprobe-${process.pid}`; +const outFile = path.join(tmpdir(), `dlgprobe-out-${process.pid}.txt`); +const workDir = path.join(tmpdir(), `dlgprobe${process.pid}`); +rmSync(outFile, { force: true }); +const since = Date.now(); + +const run = await launchOnDesktop({ + exe: path.resolve(exe), + desktop, + env: { ...process.env, TBRUN_OUT: outFile, PROBE: probe, PROBE_TAG: String(process.pid) }, +}); + +// With SHOTS=: poll the desktop, and photograph each new visible dialog or menu window. +const shotsDir = process.env.SHOTS; +let polling = !!shotsDir; +const shotLog = []; +const winLog = []; +const seenAny = new Set(); +const seenShot = new Set(); +if (shotsDir) { + mkdirSync(shotsDir, { recursive: true }); + let n = 0; + (async () => { + while (polling) { + try { + for (const w of await desktopWindows(desktop, { children: false })) { + if (w.visible && !seenAny.has(w.hwnd)) { + seenAny.add(w.hwnd); + winLog.push(`${w.class} exe=${w.exe} title=${JSON.stringify(w.title)}`); + } + if ((w.class === "#32770" || w.class === "#32768") && w.visible && !seenShot.has(w.hwnd)) { + seenShot.add(w.hwnd); + const name = `${String(++n).padStart(2, "0")}-${(w.title || "untitled").replace(/[^A-Za-z0-9]+/g, "_")}.png`; + try { + await captureWindow(desktop, w.hwnd, path.join(shotsDir, name)); + shotLog.push(name); + } catch (e) { + shotLog.push(`${name} FAILED ${e.message}`); + } + } + } + } catch { + // the desktop may be gone + } + await new Promise((r) => setTimeout(r, 300)); + } + })(); +} + +let timer; +const timedOut = await Promise.race([ + run.exited.then(() => false), + new Promise((r) => { + timer = setTimeout(() => r(true), timeoutMs); + }), +]); +clearTimeout(timer); +polling = false; +let leftover = []; +if (timedOut) { + try { + leftover = await desktopWindows(desktop, { children: false }); + } catch (e) { + leftover = [{ class: "?", title: String(e.message), visible: true }]; + } + killTree(run.pid); + run.launcher.kill(); +} +const { code } = await run.finished; +process.stdout.write(existsSync(outFile) ? readFileSync(outFile, "utf8") : ""); +const f = timedOut ? null : recordedFault(path.resolve(exe), since); +const hex = code !== null && (code < 0 || code > 0xffff) ? ` (0x${(code >>> 0).toString(16)})` : ""; +console.log(`--- exit code: ${timedOut ? `(still running after ${timeoutS}s, ended)` : code}${hex}`); +if (f) console.log(`--- fault recorded: ${faultText(f)}`); +if (shotLog.length) console.log(`--- shots: ${shotLog.join(", ")}`); +if (winLog.length) console.log(`--- visible windows seen on the private desktop:\n ${winLog.join("\n ")}`); +if (timedOut) { + console.log("--- windows left on the desktop (visible ones):"); + for (const w of leftover.filter((x) => x.visible)) console.log(` ${w.class} title=${JSON.stringify(w.title)}`); +} +rmSync(outFile, { force: true }); +rmSync(workDir, { recursive: true, force: true }); +process.exitCode = timedOut || code !== 0 ? 1 : 0; diff --git a/test/package-probes/TBMANLIB_Dialog/src/Auto.twin b/test/package-probes/TBMANLIB_Dialog/src/Auto.twin new file mode 100644 index 00000000..b484aaae --- /dev/null +++ b/test/package-probes/TBMANLIB_Dialog/src/Auto.twin @@ -0,0 +1,471 @@ +' Auto: the dialog driver of the TBMANLIB_Dialog probe. +' +' A modal common dialog (GetOpenFileName, GetSaveFileName, SHBrowseForFolder) runs its own +' message loop on the calling thread, and that loop dispatches thread timers. So the probe +' arms a timer (Arm), calls the dialog, and the timer procedure -- running inside the dialog's +' loop -- finds the dialog and its children, records them, types into the file-name box, presses +' a button and answers the message boxes the dialog raises (Confirm Save As, File not found...). +' Disarm kills the timer once the dialog call has returned. +' +' Every episode is short: 300 ms per tick and gMaxTicks ticks (12, so 3.6 s). A dialog that has +' not finished by then is cancelled and the failure recorded ("TIMED OUT"); a dialog that +' cannot even be cancelled ends the process (ExitProcess 98), so a run never hangs. +' With PROBE_SHOTS=1 (see InitFromEnv) the driver waits longer before it acts, so that the +' outside runner (run-exe.mjs) can photograph each window. +' +' The action of an episode (the last argument of Arm): +' 1 type the text (if any) and press button 1 (Open, Save, OK) +' 2 press Cancel (button 2) +' 3 press nothing: the dialog is left alone, then cancelled as "still open" +' 4 click the right edge of button 1, which opens the drop-down of a split button +' +' Every line is written with TbRun.Out, so that it reaches the file the runner reads at once. + +Module Auto + + Private Type RECT4 + Left As Long + Top As Long + Right As Long + Bottom As Long + End Type + + Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr + Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal uIDEvent As LongPtr) As Long + Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long + Private Declare PtrSafe Function GetCurrentDirectoryW Lib "kernel32" (ByVal nBufferLength As Long, ByVal lpBuffer As LongPtr) As Long + Private Declare PtrSafe Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long) + Private Declare PtrSafe Function EnumThreadWindows Lib "user32" (ByVal dwThreadId As Long, ByVal lpfn As LongPtr, ByVal lParam As LongPtr) As Long + Private Declare PtrSafe Function EnumChildWindows Lib "user32" (ByVal hWndParent As LongPtr, ByVal lpEnumFunc As LongPtr, ByVal lParam As LongPtr) As Long + Private Declare PtrSafe Function GetClassNameW Lib "user32" (ByVal hWnd As LongPtr, ByVal lpClassName As LongPtr, ByVal nMaxCount As Long) As Long + Private Declare PtrSafe Function GetClientRect Lib "user32" (ByVal hWnd As LongPtr, ByRef lpRect As RECT4) As Long + Private Declare PtrSafe Function IsWindowVisible Lib "user32" (ByVal hWnd As LongPtr) As Long + Private Declare PtrSafe Function IsWindowEnabled Lib "user32" (ByVal hWnd As LongPtr) As Long + Private Declare PtrSafe Function IsWindow Lib "user32" (ByVal hWnd As LongPtr) As Long + Private Declare PtrSafe Function GetDlgCtrlID Lib "user32" (ByVal hWnd As LongPtr) As Long + Private Declare PtrSafe Function GetParent Lib "user32" (ByVal hWnd As LongPtr) As LongPtr + Private Declare PtrSafe Function GetDlgItem Lib "user32" (ByVal hDlg As LongPtr, ByVal nIDDlgItem As Long) As LongPtr + Private Declare PtrSafe Function EndMenu Lib "user32" () As Long + Private Declare PtrSafe Function SendMessageW Lib "user32" (ByVal hWnd As LongPtr, ByVal Msg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr + Private Declare PtrSafe Function PostMessageW Lib "user32" (ByVal hWnd As LongPtr, ByVal Msg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As Long + + Private Const WM_GETTEXT As Long = &HD + Private Const WM_GETTEXTLENGTH As Long = &HE + Private Const WM_SETTEXT As Long = &HC + Private Const WM_CLOSE As Long = &H10 + Private Const WM_LBUTTONDOWN As Long = &H201 + Private Const WM_LBUTTONUP As Long = &H202 + Private Const BM_CLICK As Long = &HF5 + Private Const TDM_CLICK_BUTTON As Long = &H466 + Private Const CB_GETCOUNT As Long = &H146 + Private Const CB_GETCURSEL As Long = &H147 + Private Const CB_GETLBTEXT As Long = &H148 + Private Const CB_GETLBTEXTLEN As Long = &H149 + + Private Const TICK_MS As Long = 300 + + ' The folder the probe replaces in every line it writes, so that no path of the machine is printed. + Public gRoot As String + + ' What the last episode saw, for the probe to print. + Public gSeenMain As Boolean ' a file or folder dialog appeared + Public gStayedOpen As Boolean ' it was still open after the action (a message box refused it) + Public gTimedOut As Boolean + Public gMsgCount As Long ' message boxes the dialog raised + Public gTicks As Long ' ticks the episode lasted: 0 means the call returned within 300 ms + + ' Settings the probe makes before an episode. + Public gDump As Boolean ' list every child window of the dialog + Public gMsgSeq(0 To 7) As Long ' the button each message box is answered with: 1 OK, 2 Cancel, 6 Yes, 7 No + Public gMsgSeqN As Long + + Public gMaxTicks As Long = 12 + Public gMsgDelay As Long = 0 ' ticks before a message box is answered + Public gMainDelay As Long = 2 ' ticks before the dialog is acted on + + Private mTag As String + Private mDoType As Boolean + Private mTypeText As String + Private mAct As Long + Private mTimerId As LongPtr + Private mTick As Long + Private mSeen(0 To 63) As LongPtr + Private mSeenN As Long + Private mMainH As LongPtr + Private mMainTick As Long + Private mActedTick As Long + Private mMsgH As LongPtr + Private mMsgTick As Long + Private mMsgDone As Boolean + Private mMsgWant As Long + Private mTop(0 To 63) As LongPtr + Private mTopN As Long + Private mChH(0 To 255) As LongPtr + Private mChN As Long + + '========================================================================================= + ' Output + '========================================================================================= + + Public Sub Say(ByVal s As String) + If Len(gRoot) > 0 Then s = Replace(s, gRoot, "") + TbRun.Out s + End Sub + + ' A string in brackets, with its length; a null character is shown as . + Public Function Q(ByVal s As String) As String + Q = "[" & Replace(s, vbNullChar, "") & "] len=" & Len(s) + End Function + + ' The current directory of the process, from the API (CurDir is VB's own view of it). + Public Function CurDirApi() As String + Dim s As String = String$(520, 0) + Dim n As Long = GetCurrentDirectoryW(520, StrPtr(s)) + CurDirApi = Left$(s, n) + End Function + + '========================================================================================= + ' Arming + '========================================================================================= + + Public Sub InitFromEnv() + If Environ$("PROBE_SHOTS") = "1" Then + gMsgDelay = 8 + gMainDelay = 8 + gMaxTicks = 30 + End If + End Sub + + Public Sub Arm(ByVal tag As String, ByVal doType As Boolean, ByVal typeText As String, ByVal act As Long) + mTag = tag + mDoType = doType + mTypeText = typeText + mAct = act + mTick = 0 + mSeenN = 0 + mMainH = 0 + mMainTick = 0 + mActedTick = 0 + mMsgH = 0 + gMsgCount = 0 + gSeenMain = False + gStayedOpen = False + gTimedOut = False + gTicks = 0 + mTimerId = SetTimer(0, 0, TICK_MS, AddressOf TimerProc) + End Sub + + Public Sub Disarm() + If mTimerId <> 0 Then KillTimer 0, mTimerId + mTimerId = 0 + gTicks = mTick + End Sub + + Public Sub MsgSeq(ParamArray ids() As Variant) + Dim i As Long + gMsgSeqN = 0 + For i = LBound(ids) To UBound(ids) + If gMsgSeqN <= 7 Then + gMsgSeq(gMsgSeqN) = CLng(ids(i)) + gMsgSeqN = gMsgSeqN + 1 + End If + Next + End Sub + + '========================================================================================= + ' Windows + '========================================================================================= + + Public Function EnumTopProc(ByVal hWnd As LongPtr, ByVal lParam As LongPtr) As Long + If mTopN <= 63 Then + mTop(mTopN) = hWnd + mTopN = mTopN + 1 + End If + EnumTopProc = 1 + End Function + + Public Function EnumChildProc(ByVal hWnd As LongPtr, ByVal lParam As LongPtr) As Long + If mChN <= 255 Then + mChH(mChN) = hWnd + mChN = mChN + 1 + End If + EnumChildProc = 1 + End Function + + Private Function ClassOf(ByVal h As LongPtr) As String + Dim s As String = String$(256, 0) + Dim n As Long = GetClassNameW(h, StrPtr(s), 256) + ClassOf = Left$(s, n) + End Function + + Private Function TextOf(ByVal h As LongPtr) As String + Dim n As Long = CLng(SendMessageW(h, WM_GETTEXTLENGTH, 0, 0)) + Dim s As String = String$(n + 2, 0) + n = CLng(SendMessageW(h, WM_GETTEXT, n + 1, StrPtr(s))) + TextOf = Left$(s, n) + End Function + + ' All descendants of h, copied into list(); returns how many. (EnumChildProc fills a shared + ' array, so a caller that nests another enumeration has to copy first.) + Private Function Children(ByVal h As LongPtr, ByRef list() As LongPtr) As Long + Dim i As Long + mChN = 0 + EnumChildWindows h, AddressOf EnumChildProc, 0 + For i = 0 To mChN - 1 + list(i) = mChH(i) + Next + Children = mChN + End Function + + Private Function WasSeen(ByVal h As LongPtr) As Boolean + Dim i As Long + For i = 0 To mSeenN - 1 + If mSeen(i) = h Then WasSeen = True : Exit Function + Next + End Function + + Private Sub MarkSeen(ByVal h As LongPtr) + If mSeenN <= 63 Then + mSeen(mSeenN) = h + mSeenN = mSeenN + 1 + End If + End Sub + + ' A file or folder dialog has an edit box, a combo box or a tree; a message box does not. + Private Function IsMain(ByVal h As LongPtr) As Boolean + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim c As String + Dim n As Long = Children(h, list) + For i = 0 To n - 1 + c = ClassOf(list(i)) + If c = "Edit" Or c = "ComboBoxEx32" Or c = "SysTreeView32" Or c = "ComboBox" Or c = "DUIViewWndClassName" Then + IsMain = True + Exit Function + End If + Next + End Function + + Private Sub PressId(ByVal h As LongPtr, ByVal id As Long) + Dim b As LongPtr = GetDlgItem(h, id) + If b <> 0 Then + Say " press id " & id & " (" & Q(TextOf(b)) & ", ena=" & IsWindowEnabled(b) & ")" + PostMessageW b, BM_CLICK, 0, 0 + Else + Say " no button id " & id & "; WM_COMMAND" + PostMessageW h, &H111, id, 0 + End If + End Sub + + '========================================================================================= + ' Recording + '========================================================================================= + + ' Every child of the dialog, one line each (gDump). + Private Sub DumpChildren(ByVal h As LongPtr) + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim n As Long = Children(h, list) + For i = 0 To n - 1 + Say " child " & ClassOf(list(i)) & " id=0x" & Hex$(GetDlgCtrlID(list(i))) & " vis=" & IsWindowVisible(list(i)) & " ena=" & IsWindowEnabled(list(i)) & " parent=" & ClassOf(GetParent(list(i))) & " text=" & Q(TextOf(list(i))) + Next + End Sub + + ' The window title, the edit boxes, the labels, the buttons, and the items of each combo box. + Private Sub RecordMain(ByVal h As LongPtr) + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim k As Long + Dim n As Long + Dim cnt As Long + Dim ln As Long + Dim buf As String + Say " [" & mTag & "] dialog seen (tick " & mTick & ") title=" & Q(TextOf(h)) & " class=" & ClassOf(h) + If gDump Then DumpChildren h + cnt = Children(h, list) + For i = 0 To cnt - 1 + Select Case ClassOf(list(i)) + Case "Edit" + If IsWindowVisible(list(i)) <> 0 Then Say " Edit id=0x" & Hex$(GetDlgCtrlID(list(i))) & " parent=" & ClassOf(GetParent(list(i))) & " text=" & Q(TextOf(list(i))) + Case "Static" + If Len(TextOf(list(i))) > 0 And IsWindowVisible(list(i)) <> 0 Then Say " Static id=0x" & Hex$(GetDlgCtrlID(list(i))) & " text=" & Q(TextOf(list(i))) + Case "Button" + If IsWindowVisible(list(i)) <> 0 Then Say " Button id=0x" & Hex$(GetDlgCtrlID(list(i))) & " ena=" & IsWindowEnabled(list(i)) & " text=" & Q(TextOf(list(i))) + Case "ComboBox" + n = CLng(SendMessageW(list(i), CB_GETCOUNT, 0, 0)) + If n > 0 Then + Say " ComboBox id=0x" & Hex$(GetDlgCtrlID(list(i))) & " items=" & n & " cursel=" & CLng(SendMessageW(list(i), CB_GETCURSEL, 0, 0)) + For k = 0 To n - 1 + ln = CLng(SendMessageW(list(i), CB_GETLBTEXTLEN, k, 0)) + buf = String$(ln + 2, 0) + SendMessageW list(i), CB_GETLBTEXT, k, StrPtr(buf) + Say " item " & k & ": " & Q(Left$(buf, ln)) + Next + End If + End Select + Next + End Sub + + Private Sub RecordMainNow(ByVal h As LongPtr) + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim n As Long = Children(h, list) + For i = 0 To n - 1 + If ClassOf(list(i)) = "Edit" And IsWindowVisible(list(i)) <> 0 Then Say " now: Edit text=" & Q(TextOf(list(i))) + Next + End Sub + + Private Sub RecordMsg(ByVal h As LongPtr) + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim n As Long + gMsgCount = gMsgCount + 1 + Say " [" & mTag & "] message box #" & gMsgCount & " title=" & Q(TextOf(h)) + n = Children(h, list) + For i = 0 To n - 1 + Select Case ClassOf(list(i)) + Case "Static", "Button" + Say " child " & ClassOf(list(i)) & " text=" & Q(TextOf(list(i))) + End Select + Next + mMsgWant = 1 + If gMsgSeqN > 0 Then + If gMsgCount <= gMsgSeqN Then mMsgWant = gMsgSeq(gMsgCount - 1) Else mMsgWant = gMsgSeq(gMsgSeqN - 1) + End If + End Sub + + '========================================================================================= + ' Acting + '========================================================================================= + + Private Sub DoAct(ByVal h As LongPtr) + Dim list(0 To 255) As LongPtr + Dim i As Long + Dim n As Long + Dim ed As LongPtr + If mDoType Then + n = Children(h, list) + For i = 0 To n - 1 + If ClassOf(list(i)) = "Edit" And IsWindowVisible(list(i)) <> 0 Then ed = list(i) : Exit For + Next + If ed <> 0 Then + SendMessageW ed, WM_SETTEXT, 0, StrPtr(mTypeText) + Say " typed " & Q(mTypeText) & " -> now " & Q(TextOf(ed)) + Else + Say " NO EDIT BOX to type into" + End If + End If + Select Case mAct + Case 3 + Say " (no button pressed)" + Case 4 + Dim bb As LongPtr = GetDlgItem(h, 1) + Dim rc As RECT4 + GetClientRect bb, rc + Dim lp As LongPtr = ((rc.Bottom \ 2) * 65536) Or (rc.Right - 8) + PostMessageW bb, WM_LBUTTONDOWN, 1, lp + PostMessageW bb, WM_LBUTTONUP, 0, lp + Say " clicked at the right edge of button 1 (client rect 0,0," & rc.Right & "," & rc.Bottom & ")" + Case 2 + PressId h, 2 + Case Else + PressId h, 1 + End Select + End Sub + + Private Sub AnswerMsg(ByVal h As LongPtr) + Dim btn As LongPtr = GetDlgItem(h, mMsgWant) + If btn = 0 Then btn = GetDlgItem(h, 1) + If btn = 0 Then btn = GetDlgItem(h, 2) + If btn <> 0 Then + Say " answering with id " & GetDlgCtrlID(btn) & " (" & Q(TextOf(btn)) & ")" + PostMessageW btn, BM_CLICK, 0, 0 + Else + ' The Confirm Save As and Create-the-file boxes are task dialogs: their buttons have no control id. + Say " answering with TDM_CLICK_BUTTON " & mMsgWant + PostMessageW h, TDM_CLICK_BUTTON, mMsgWant, 0 + End If + End Sub + + Private Sub HandleDialog(ByVal h As LongPtr) + If Not IsMain(h) Then + If Not WasSeen(h) Then + MarkSeen h + mMsgH = h + mMsgTick = mTick + mMsgDone = False + RecordMsg h + End If + ' While a message box is up the dialog behind it is not "still open" for lack of an answer. + If mActedTick <> 0 Then mActedTick = mTick + If h = mMsgH And Not mMsgDone Then + If mTick - mMsgTick >= gMsgDelay Then + mMsgDone = True + AnswerMsg h + End If + End If + Else + If Not WasSeen(h) Then + MarkSeen h + mMainH = h + mMainTick = mTick + gSeenMain = True + RecordMain h + ElseIf h = mMainH Then + If mActedTick = 0 Then + If mTick - mMainTick >= gMainDelay Then + mActedTick = mTick + DoAct h + End If + ElseIf mTick - mActedTick >= IIf(mAct = 4, gMainDelay + 3, 3) Then + gStayedOpen = True + Say " [" & mTag & "] dialog still open " & (mTick - mActedTick) & " ticks after the action" + RecordMainNow h + EndMenu + PressId h, 2 + mActedTick = mTick + End If + End If + End If + End Sub + + ' The timer procedure: runs inside the modal loop of the dialog. + Public Sub TimerProc(ByVal hWnd As LongPtr, ByVal uMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long) + Dim i As Long + Dim h As LongPtr + Dim list(0 To 63) As LongPtr + Dim n As Long + mTick = mTick + 1 + mTopN = 0 + EnumThreadWindows GetCurrentThreadId(), AddressOf EnumTopProc, 0 + n = mTopN + For i = 0 To n - 1 + list(i) = mTop(i) + Next + For i = 0 To n - 1 + h = list(i) + If IsWindow(h) <> 0 Then + If IsWindowVisible(h) <> 0 Then + If ClassOf(h) = "#32770" Then + If mTick >= gMaxTicks Then + If Not gTimedOut Then + gTimedOut = True + Say " [" & mTag & "] TIMED OUT at tick " & mTick & ", cancelling (dialog title " & Q(TextOf(h)) & ", main dialog seen: " & gSeenMain & ")" + End If + EndMenu + PostMessageW h, TDM_CLICK_BUTTON, 2, 0 + PostMessageW h, WM_CLOSE, 0, 0 + Else + HandleDialog h + End If + End If + End If + End If + Next + If mTick >= gMaxTicks + 6 Then + Say " [" & mTag & "] STUCK: a window could not be cancelled, ending the process" + ExitProcess 98 + End If + End Sub + +End Module diff --git a/test/package-probes/TBMANLIB_Dialog/src/Probe.twin b/test/package-probes/TBMANLIB_Dialog/src/Probe.twin new file mode 100644 index 00000000..28addb6f --- /dev/null +++ b/test/package-probes/TBMANLIB_Dialog/src/Probe.twin @@ -0,0 +1,550 @@ +' Probe: what TBMANLIB_Dialog's cDialog does at run time. +' +' The exe runs Sub Main. The scenarios named by the PROBE environment variable, a +' comma-separated list of ids ("1,2,6d"), are run in the order of ScenarioIds, and each prints +' what it observed. A scenario's id is the number of the question it answers (the list below). +' +' The IDE-side [RunAfterBuild] Sub only prints: no dialog is ever opened in the IDE, and +' every dialog the probe opens is driven by Auto (the timer-based driver) from inside the exe. +' +' Scenario ids and what they ask: +' 1 ShowOpen, one file: the return value, its VarType and FileName +' 2 ShowOpen with MultiSelect: one file, then two files +' 3 Cancel in ShowOpen, ShowSave and ShowBrowseForFolder +' 4 the dialog's title with DialogTitle empty and set (and where ShowBrowseForFolder shows it) +' 5 the file-type combo after SetImageFilters, AddFilter alone and Filter = "" +' 6 ShowSave: the file-name box's initial text, DefaultExt, a long FileName +' 6d ShowSave: OverwritePrompt and the prompt's answers +' 7 FileMustExist and CreatePrompt on ShowOpen, and the boxes they raise +' 8 NoChangeDir on ShowOpen and ShowSave: the current directory before and after +' 9 ShowBrowseForFolder: typing a folder, InitialDir, NewDialogStyle +' 9e ShowBrowseForFolder with NewDialogStyle = False, left alone +' 11 OpenFile on a nonexistent path +' (There is no OpenFile("") scenario: it returns 42 and opens a File Explorer window on the +' current directory, and the COM-started explorer.exe that shows it outlives the exe, with +' its window on the private desktop. Never call OpenFile on a path that exists, or on "".) +' 12 HideReadOnly = False and ReadOnly = True: the Open dialog's child windows +' 12b the default Open dialog, for comparison +' 12c HideReadOnly = False: click the Open button's arrow, which opens its drop-down +' 13 the properties' defaults (no dialog) + +Module Probe + + Private Root As String + + [RunAfterBuild] + Public Sub Hook() + Debug.Cls + Debug.Print "hook ran in the IDE; the dialogs run only in the exe" + End Sub + + Private Const ScenarioIds As String = ",13,1,2,3,4,5,6,6d,7,8,9,9e,11,12,12b,12c," + + Public Sub Main() + Dim sel As String = "," & Replace(Environ$("PROBE"), " ", "") & "," + ' A folder of its own for each run, so that two runs at once do not meet. + Root = Environ$("TEMP") & "\dlgprobe" & Environ$("PROBE_TAG") + Auto.gRoot = Root + #If Win64 Then + Say "build: 64-bit (Win64), LenB(LongPtr)=" & LenB(CLngPtr(0)) + #Else + Say "build: 32-bit, LenB(LongPtr)=" & LenB(CLngPtr(0)) + #End If + InitFromEnv + Setup + Dim ids() As String + Dim i As Long + ids = Split(Mid$(ScenarioIds, 2, Len(ScenarioIds) - 2), ",") + For i = 0 To UBound(ids) + If InStr(sel, "," & ids(i) & ",") > 0 Then RunScenario ids(i) + Next + Say "== done" + End Sub + + Private Sub RunScenario(ByVal id As String) + On Error GoTo EH + ChDir Root & "\cwd" + Select Case id + Case "1": S1 + Case "2": S2 + Case "3": S3 + Case "4": S4 + Case "5": S5 + Case "6": S6 + Case "6d": S6d + Case "7": S7 + Case "8": S8 + Case "9": S9 + Case "9e": S9e + Case "11": S11 + Case "12": S12 + Case "12b": S12b + Case "12c": S12c + Case "13": S13 + End Select + Exit Sub +EH: + Disarm + Say " ERROR in scenario " & id & ": " & Err.Number & " " & Err.Description + End Sub + + '========================================================================================= + ' Helpers + '========================================================================================= + + Private Sub MkDirIfMissing(ByVal p As String) + If Len(Dir$(p, vbDirectory)) = 0 Then MkDir p + End Sub + + Private Sub PutFile(ByVal p As String, ByVal text As String) + Dim f As Integer = FreeFile + Open p For Output As #f + Print #f, text + Close #f + End Sub + + ' \data holds a.txt, b.txt and exist.txt; \other holds c.txt; \cwd is the + ' current directory the scenarios start in. + Private Sub Setup() + MkDirIfMissing Root + MkDirIfMissing Root & "\data" + MkDirIfMissing Root & "\other" + MkDirIfMissing Root & "\cwd" + PutFile Root & "\data\a.txt", "a" + PutFile Root & "\data\b.txt", "b" + PutFile Root & "\data\exist.txt", "exist" + PutFile Root & "\other\c.txt", "c" + Dim nm As Variant + For Each nm In Array("data\out.txt", "data\out", "data\out.dat", "data\nosuch.txt", "other\new.txt") + If Len(Dir$(Root & "\" & nm)) > 0 Then Kill Root & "\" & nm + Next + End Sub + + ' A value as the probe prints it: its VarType, its type name and its text, or each element of an array. + Private Function Desc(ByVal x As Variant) As String + Dim i As Long + Dim s As String + If IsArray(x) Then + s = "Array VarType=" & VarType(x) & " " & LBound(x) & " To " & UBound(x) + For i = LBound(x) To UBound(x) + s = s & " [" & i & "]=" & Q(CStr(x(i))) + Next + Desc = s + Else + Desc = "VarType=" & VarType(x) & " " & TypeName(x) & " " & Q(CStr(x)) + End If + End Function + + ' What the driver saw during the episode that has just ended. + Private Sub Wrap(ByVal what As String) + Say " after " & what & ": dialogSeen=" & gSeenMain & " stayedOpen=" & gStayedOpen & " timedOut=" & gTimedOut & " msgBoxes=" & gMsgCount + End Sub + + '========================================================================================= + ' Scenarios + '========================================================================================= + + Private Sub S1() + Say "== 1 ShowOpen one file" + Dim d As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + Arm "S1", True, "a.txt", 1 + r = d.ShowOpen() + Disarm + Say " result: " & Desc(r) + Say " FileName=" & Q(d.FileName) & " InitialDir=" & Q(d.InitialDir) + Wrap "S1" + End Sub + + Private Sub S2() + Say "== 2 ShowOpen MultiSelect" + Dim d As New cDialog + Dim d2 As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + d.MultiSelect = True + Arm "S2a", True, "a.txt", 1 + r = d.ShowOpen() + Disarm + Say " (a) one file result: " & Desc(r) + Say " (a) FileName=" & Q(d.FileName) + Wrap "S2a" + d2.InitialDir = Root & "\data" + d2.MultiSelect = True + Arm "S2b", True, """a.txt"" ""b.txt""", 1 + r = d2.ShowOpen() + Disarm + Say " (b) two files result: " & Desc(r) + Say " (b) FileName=" & Q(d2.FileName) + Wrap "S2b" + End Sub + + Private Sub S3() + Say "== 3 Cancel" + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim r As Variant + Dim s As String + d.InitialDir = Root & "\data" + d.FileName = "keep.txt" + Arm "S3open", False, "", 2 + r = d.ShowOpen() + Disarm + Say " ShowOpen cancel: " & Desc(r) & " FileName=" & Q(d.FileName) & " InitialDir=" & Q(d.InitialDir) + Wrap "S3open" + d2.InitialDir = Root & "\data" + d2.FileName = "keep.txt" + Arm "S3save", False, "", 2 + s = d2.ShowSave() + Disarm + Say " ShowSave cancel: " & Desc(s) & " FileName=" & Q(d2.FileName) & " InitialDir=" & Q(d2.InitialDir) + Wrap "S3save" + d3.InitialDir = Root & "\data" + d3.FileName = "keep.txt" + Arm "S3browse", False, "", 2 + s = d3.ShowBrowseForFolder() + Disarm + Say " ShowBrowseForFolder cancel: " & Desc(s) & " FileName=" & Q(d3.FileName) & " InitialDir=" & Q(d3.InitialDir) + Wrap "S3browse" + End Sub + + Private Sub S4() + Say "== 4 Title" + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim r As Variant + Dim s As String + d.InitialDir = Root & "\data" + Arm "S4open-default", False, "", 2 + r = d.ShowOpen() + Disarm + d.DialogTitle = "My Open Title" + Arm "S4open-set", False, "", 2 + r = d.ShowOpen() + Disarm + d2.InitialDir = Root & "\data" + Arm "S4save-default", False, "", 2 + s = d2.ShowSave() + Disarm + d2.DialogTitle = "My Save Title" + Arm "S4save-set", False, "", 2 + s = d2.ShowSave() + Disarm + Arm "S4browse-default", False, "", 2 + s = d3.ShowBrowseForFolder() + Disarm + d3.DialogTitle = "My Browse Title" + Arm "S4browse-set", False, "", 2 + s = d3.ShowBrowseForFolder() + Disarm + End Sub + + Private Sub S5() + Say "== 5 Filters" + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + d.SetImageFilters + Say " Filter after SetImageFilters = " & Q(d.Filter) + Arm "S5image", False, "", 2 + r = d.ShowOpen() + Disarm + d2.InitialDir = Root & "\data" + d2.AddFilter "Text (*.txt)", "*.txt" + Say " Filter after AddFilter alone = " & Q(d2.Filter) + Arm "S5add", False, "", 2 + r = d2.ShowOpen() + Disarm + d3.InitialDir = Root & "\data" + d3.Filter = "" + Arm "S5empty", False, "", 2 + r = d3.ShowOpen() + Disarm + End Sub + + ' 6a, 6b, 6c: ShowSave's file-name box, DefaultExt, a long FileName. + Private Sub S6() + Say "== 6 ShowSave" + Dim d As New cDialog + Dim s As String + Dim e As Variant + Dim nl As Variant + Dim d2 As cDialog + Dim d3 As New cDialog + Dim d4 As cDialog + Dim en As Long + Dim ed As String + d.InitialDir = Root & "\data" + d.FileName = "" + Arm "S6a-empty", False, "", 2 + s = d.ShowSave() + Disarm + d.FileName = "x.txt" + Arm "S6a-x.txt", False, "", 2 + s = d.ShowSave() + Disarm + ' (b) DefaultExt + For Each e In Array("txt", ".txt", "") + Set d2 = New cDialog + d2.InitialDir = Root & "\data" + d2.DefaultExt = CStr(e) + Arm "S6b-ext=" & CStr(e), True, "out", 1 + s = d2.ShowSave() + Disarm + Say " (b) DefaultExt=" & Q(CStr(e)) & " typed [out] -> " & Desc(s) & " FileName=" & Q(d2.FileName) + Wrap "S6b" + Next + ' (c) a long FileName: the source pads it to 260 characters with Space$(260 - Len) + d3.InitialDir = Root & "\data" + d3.FileName = String$(300, "x") + Arm "S6c-300", False, "", 2 + On Error Resume Next + s = d3.ShowSave() + en = Err.Number + ed = Err.Description + On Error GoTo 0 + Disarm + Say " (c) FileName length 300: Err.Number=" & en & " Description=" & Q(ed) & " dialogSeen=" & gSeenMain & " timerTicks=" & gTicks + For Each nl In Array(250, 259, 260, 261) + Set d4 = New cDialog + d4.InitialDir = Root & "\data" + d4.FileName = String$(CLng(nl), "y") + Arm "S6c-" & nl, False, "", 2 + On Error Resume Next + s = d4.ShowSave() + en = Err.Number + ed = Err.Description + On Error GoTo 0 + Disarm + Say " (c) FileName length " & nl & ": Err.Number=" & en & " Description=" & Q(ed) & " dialogSeen=" & gSeenMain & " timerTicks=" & gTicks & " result=" & Q(s) + Next + End Sub + + ' 6d: the overwrite prompt, answered Yes, No, and with OverwritePrompt = False. + Private Sub S6d() + Say "== 6d ShowSave overwrite prompt" + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim s As String + d.InitialDir = Root & "\data" + MsgSeq 6 + Arm "S6d-yes", True, "exist.txt", 1 + s = d.ShowSave() + Disarm + Say " (d) OverwritePrompt=" & d.OverwritePrompt & " answer Yes -> " & Desc(s) & " FileName=" & Q(d.FileName) + Wrap "S6d-yes" + d2.InitialDir = Root & "\data" + MsgSeq 7 + Arm "S6d-no", True, "exist.txt", 1 + s = d2.ShowSave() + Disarm + Say " (d) answer No -> " & Desc(s) & " FileName=" & Q(d2.FileName) + Wrap "S6d-no" + d3.InitialDir = Root & "\data" + d3.OverwritePrompt = False + MsgSeq 1 + Arm "S6d-noprompt", True, "exist.txt", 1 + s = d3.ShowSave() + Disarm + Say " (d) OverwritePrompt=False -> " & Desc(s) + Wrap "S6d-noprompt" + End Sub + + Private Sub S7() + Say "== 7 FileMustExist / CreatePrompt" + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim d4 As New cDialog + Dim d5 As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + MsgSeq 1 + Arm "S7a-mustexist", True, "nosuch.txt", 1 + r = d.ShowOpen() + Disarm + Say " (a) FileMustExist=" & d.FileMustExist & " typed nosuch.txt -> " & Desc(r) & " FileName=" & Q(d.FileName) + Wrap "S7a" + d2.InitialDir = Root & "\data" + d2.FileMustExist = False + Arm "S7b-notmust", True, "nosuch.txt", 1 + r = d2.ShowOpen() + Disarm + Say " (b) FileMustExist=False typed nosuch.txt -> " & Desc(r) & " FileName=" & Q(d2.FileName) + Wrap "S7b" + d3.InitialDir = Root & "\data" + d3.CreatePrompt = True + MsgSeq 6 + Arm "S7c-create-must", True, "nosuch.txt", 1 + r = d3.ShowOpen() + Disarm + Say " (c1) CreatePrompt=True FileMustExist=True, Yes -> " & Desc(r) + Wrap "S7c1" + d4.InitialDir = Root & "\data" + d4.CreatePrompt = True + d4.FileMustExist = False + MsgSeq 6 + Arm "S7c-create-notmust", True, "nosuch.txt", 1 + r = d4.ShowOpen() + Disarm + Say " (c2) CreatePrompt=True FileMustExist=False, Yes -> " & Desc(r) + Wrap "S7c2" + d5.InitialDir = Root & "\data" + d5.CreatePrompt = True + d5.FileMustExist = False + MsgSeq 7 + Arm "S7c-create-no", True, "nosuch.txt", 1 + r = d5.ShowOpen() + Disarm + Say " (c3) CreatePrompt=True FileMustExist=False, No -> " & Desc(r) + Wrap "S7c3" + Say " file nosuch.txt exists afterwards: " & (Len(Dir$(Root & "\data\nosuch.txt")) > 0) + End Sub + + Private Sub S8() + Say "== 8 NoChangeDir" + Dim ncd As Variant + Dim r As Variant + Dim s As String + Dim d As cDialog + Dim d2 As cDialog + For Each ncd In Array(False, True) + ChDir Root & "\cwd" + Set d = New cDialog + d.InitialDir = Root & "\other" + d.NoChangeDir = CBool(ncd) + Say " ShowOpen NoChangeDir=" & ncd & ": before CurDir=" & Q(CurDir) & " api=" & Q(CurDirApi) + Arm "S8open-" & ncd, True, "c.txt", 1 + r = d.ShowOpen() + Disarm + Say " result " & Desc(r) & "; after CurDir=" & Q(CurDir) & " api=" & Q(CurDirApi) + ChDir Root & "\cwd" + Set d2 = New cDialog + d2.InitialDir = Root & "\other" + d2.NoChangeDir = CBool(ncd) + Say " ShowSave NoChangeDir=" & ncd & ": before CurDir=" & Q(CurDir) & " api=" & Q(CurDirApi) + Arm "S8save-" & ncd, True, "new.txt", 1 + s = d2.ShowSave() + Disarm + Say " result " & Desc(s) & "; after CurDir=" & Q(CurDir) & " api=" & Q(CurDirApi) + Next + End Sub + + Private Sub S9() + Say "== 9 ShowBrowseForFolder" + Dim s As String + Dim d As New cDialog + Dim d2 As New cDialog + Dim d3 As New cDialog + Dim d4 As New cDialog + gDump = True + Arm "S9a-type", True, Root & "\other", 1 + s = d.ShowBrowseForFolder() + Disarm + gDump = False + Say " (a) typed folder -> " & Desc(s) & " InitialDir=" & Q(d.InitialDir) + Wrap "S9a" + Arm "S9b-nothing", False, "", 1 + s = d2.ShowBrowseForFolder() + Disarm + Say " (b) no InitialDir, OK pressed with nothing typed -> " & Desc(s) + Wrap "S9b" + d3.InitialDir = Root & "\data" + Arm "S9c-initialdir", False, "", 1 + s = d3.ShowBrowseForFolder() + Disarm + Say " (c) InitialDir=data, OK pressed with nothing typed -> " & Desc(s) + Wrap "S9c" + d4.NewDialogStyle = False + Arm "S9d-oldstyle", False, "", 1 + s = d4.ShowBrowseForFolder() + Disarm + Say " (d) NewDialogStyle=False, OK pressed with nothing selected -> " & Desc(s) + Wrap "S9d" + End Sub + + Private Sub S9e() + Say "== 9e ShowBrowseForFolder, NewDialogStyle=False, nothing pressed" + Dim s As String + Dim d As New cDialog + d.NewDialogStyle = False + d.DialogTitle = "Old Style Title" + gDump = True + Arm "S9e-oldstyle-idle", False, "", 3 + s = d.ShowBrowseForFolder() + Disarm + gDump = False + Say " (e) result after the driver cancelled -> " & Desc(s) + Wrap "S9e" + End Sub + + Private Sub S11() + Say "== 11 OpenFile on a nonexistent path" + Dim d As New cDialog + Dim r As Long + MsgSeq 1 + Arm "S11a", False, "", 2 + r = d.OpenFile(Root & "\nosuchfolder\nosuchfile.xyz") + Disarm + Say " OpenFile(nonexistent path) = " & r + Wrap "S11a" + End Sub + + Private Sub S12() + Say "== 12 HideReadOnly = False, then ReadOnly = True: the Open dialog's controls" + Dim d As New cDialog + Dim d2 As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + d.HideReadOnly = False + gDump = True + Arm "S12-hide-false", False, "", 2 + r = d.ShowOpen() + Disarm + d2.InitialDir = Root & "\data" + d2.HideReadOnly = False + d2.ReadOnly = True + Arm "S12-readonly-true", False, "", 2 + r = d2.ShowOpen() + Disarm + gDump = False + End Sub + + Private Sub S12b() + Say "== 12b default Open dialog (HideReadOnly = True), for comparison" + Dim d As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + Arm "S12b-default", False, "", 3 + r = d.ShowOpen() + Disarm + End Sub + + Private Sub S12c() + Say "== 12c HideReadOnly = False, click the Open button's arrow" + Dim d As New cDialog + Dim r As Variant + d.InitialDir = Root & "\data" + d.HideReadOnly = False + Arm "S12c-arrow", False, "", 4 + r = d.ShowOpen() + Disarm + Say " result: " & Desc(r) + Wrap "S12c" + End Sub + + Private Sub S13() + Say "== 13 defaults" + Dim d As New cDialog + Say " DialogTitle=" & Q(d.DialogTitle) & " InitialDir=" & Q(d.InitialDir) & " DefaultExt=" & Q(d.DefaultExt) & " FileName=" & Q(d.FileName) & " Filter=" & Q(d.Filter) + Say " MultiSelect=" & d.MultiSelect & " OverwritePrompt=" & d.OverwritePrompt & " PathMustExist=" & d.PathMustExist & " FileMustExist=" & d.FileMustExist & " HideReadOnly=" & d.HideReadOnly & " ReadOnly=" & d.ReadOnly + Say " NoChangeDir=" & d.NoChangeDir & " CreatePrompt=" & d.CreatePrompt & " NewDialogStyle=" & d.NewDialogStyle + End Sub + +End Module