tagwire
v0.1.0
Published
A small semantic tag protocol and TypeScript codec for Node.js and browsers.
Maintainers
Readme
tagwire
A small semantic tag protocol and TypeScript codec for Node.js and browsers. Convert one structured value into one tag, and back, with no runtime dependencies.
import { parse, serialize } from "tagwire";
const tag = {
name: "skill",
attributes: { path: "~/.skills/media-generation" },
content: "媒体生成技能",
};
const text = serialize(tag);
// <skill path="~/.skills/media-generation">媒体生成技能</skill>
parse(text); // Same name, attributes, and content as tag.The tag name expresses meaning, attributes carry metadata, and the body carries readable text. Applications define their own vocabulary:
<camera-motion>镜头上摇</camera-motion>
<duration unit="s">6</duration>
<skill path="~/.skills/media-generation">媒体生成技能</skill>
<node id="project-a/node-x">节点 x</node>These are four separate values, not one valid tagwire input. Ordinary text, multiple tags, and nested tags are outside the format:
parse("Please use <skill>Media generation</skill>."); // Throws.
parse("<skill>A</skill><skill>B</skill>"); // Throws.
parse("<skill><name>A</name></skill>"); // Throws.Install and use
npm install tagwireThe package publishes ESM JavaScript and TypeScript declarations. Node.js 18 or later and browser projects with an ESM-aware bundler can use the imports above. The built module also works directly in a browser when its dist directory is served by your application:
<script type="module">
import { parse } from "/vendor/tagwire/dist/index.js";
console.log(parse('<duration unit="s">6</duration>'));
</script>The codec uses no Node.js, DOM, filesystem, or network APIs. It does not render capsules, scan messages, load skills, or resolve references and paths. For example, ~ remains a literal character.
Protocol
This README is the protocol specification. tagwire uses a deliberately restricted XML-like syntax; it is not a general XML parser or an existing industry standard.
Structured representation
interface Tag {
readonly name: string;
readonly attributes: Readonly<Record<string, string>>;
readonly content: string;
}All three fields are required. Use {} for no attributes and "" for empty content. Attribute values and content are literal, unescaped strings. The serializer performs escaping; callers must not pre-escape their values.
Attributes are record objects, including objects with a null prototype; only their own enumerable string-keyed properties are metadata. Arrays and built-in containers such as Map, Set, and Date are rejected.
The protocol does not reserve business tag names or define an allowlist. Applications own each tag's meaning, permitted attributes, required fields, value validation, reference resolution, and rendering. Recognizing valid tagwire syntax does not validate those application rules or authorize an action.
Accepted text
| Rule | Contract |
| --- | --- |
| Document | Exactly one complete tag. Leading and trailing XML whitespace are accepted and excluded from the result. |
| Names | Tag and attribute names match [a-z][a-z0-9]*(?:-[a-z0-9]+)*: lowercase ASCII, optionally separated by single hyphens. Names are case-sensitive. |
| Attributes | String values enclosed in either single or double quotes. Attribute names must be unique. XML whitespace separates attributes and may surround =. |
| Content | Plain text, including empty or multiline text. No child tags. |
| Empty tag | <skill/> and <skill></skill> both produce empty content. |
| Whitespace | XML whitespace is space, tab, LF, or CR. Literal whitespace inside attribute values and content is preserved; there is no XML newline or attribute-value normalization. |
| Escaping | The five predefined entities &, <, >, ", and ' are supported, as are decimal A and hexadecimal A character references. Hexadecimal uses lowercase x; hex digits may use either case. Entities decode exactly once. |
| Characters | Literal and decoded characters must satisfy XML 1.0's character range: U+0009, U+000A, U+000D, U+0020–U+D7FF, U+E000–U+FFFD, or U+10000–U+10FFFF. Unpaired UTF-16 surrogates are invalid. |
| Rejected syntax | Unknown or malformed entities, raw &, raw < inside values or content, raw ]]> in content, duplicate attributes, and mismatched closing names. |
| Unsupported constructs | XML declarations, processing instructions, DTDs, comments, CDATA, namespace-qualified names such as ns:tag, and nested elements. No namespace resolution is performed; xmlns is ordinary metadata. |
Escape markup when it is literal content:
<note>Use <skill> as an example, not as a child element.</note>Its parsed content is Use <skill> as an example, not as a child element. Markdown and backticks have no special meaning in this protocol.
Canonical serialization and round trips
serialize validates its input and emits one canonical representation:
- Attributes use double quotes and are sorted by ASCII name order.
- Every tag has an explicit closing tag, including empty content.
- Content escapes
&,<, and>. - Attribute values additionally escape
", tab, and LF. CR is emitted as in both attribute values and content.
parse(serialize(tag)) preserves the tag's name, attribute key/value pairs, and content. Attribute insertion order is not meaningful. serialize(parse(text)) may normalize quoting, attribute order, entity spelling, outer whitespace, and empty-tag syntax; byte-for-byte source preservation is not promised.
For example, &lt; decodes to the literal string <, not to <. Serializing that string emits &lt; again.
API and errors
import {
parse,
serialize,
TagwireError,
type Tag,
type TagwireErrorCode,
} from "tagwire";
// parse(text: string): Tag
// serialize(tag: Tag): stringInvalid inputs throw TagwireError, an Error subclass with a stable code and an optional offset. Parse errors in text include a zero-based UTF-16 offset into the original input. Input-type errors and serialization errors have no offset. Error messages are descriptive, not a machine-readable contract.
| Code | Meaning |
| --- | --- |
| INVALID_INPUT | Wrong input type or structured shape. |
| INVALID_NAME | Tag or attribute name violates the name grammar. |
| INVALID_SYNTAX | Text violates the tag syntax, including extra content or nested elements. |
| INVALID_ENTITY | Unsupported or malformed entity/reference. |
| INVALID_CHARACTER | A literal or decoded character falls outside the allowed range. |
| DUPLICATE_ATTRIBUTE | The same attribute occurs more than once. |
try {
const tag = parse(receivedText);
// Apply your application's tag/attribute rules before using this value.
} catch (error) {
if (!(error instanceof TagwireError)) throw error;
console.error(error.code, error.offset, error.message);
}Unknown business names are valid syntax: parse("<ticket>Support request</ticket>") succeeds. There is no recovery into ordinary text or partially parsed output. The caller decides how to present or handle invalid inputs.
AI integration
For AI assistants working in a consuming project, explicitly link this README from that project's AGENTS.md or equivalent instructions. Installing an npm package does not guarantee automatic discovery of its documentation. Keep application-specific vocabulary and permissions in the consuming project's own instructions.
For a model reading serialized tags, provide the meanings and allowed uses of the tags it may encounter. Meaningful names and readable content help interpretation, but a tag alone neither loads a referenced resource nor executes an instruction.
Develop and publish
pnpm install
pnpm checkpnpm check runs lint, type checking, build, and tests. Individual scripts are pnpm build, pnpm test, pnpm typecheck, and pnpm format. The tests exercise the protocol boundaries and round-trip guarantees.
The package is licensed under MIT. Publishing is manual. Confirm the package version and metadata, then create and inspect the actual release archive:
npm pack
tar -tzf tagwire-*.tgzprepack runs the checks before creating the archive. Install the resulting tarball into a consuming project to verify its public ESM entry point and declarations.
Log in to the public npm registry and confirm the publishing account:
npm login --registry=https://registry.npmjs.org/
npm whoami --registry=https://registry.npmjs.org/Publish the inspected archive with npm publish ./tagwire-<version>.tgz --access public --registry=https://registry.npmjs.org/, replacing <version> with the archive's version. Follow npm's authentication prompts. There is no GitHub publishing workflow.
