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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,33 @@ for (const event of parser) {
}
```


##### Cursor-style Synchronous Parsing (StaxXmlCursorReaderSync)

```typescript
import { StaxXmlCursorReaderSync, XmlEventType } from 'stax-xml';

const reader = new StaxXmlCursorReaderSync('<root id="1"><item>text</item></root>');

while (reader.read()) {
const event = reader.requireEvent();

if (event.type === XmlEventType.START_ELEMENT) {
console.log(event.name, event.getAttributeCount());
console.log(event.getAttributeValue('id'));
}

if (event.type === XmlEventType.CHARACTERS) {
console.log(event.text);
}
}
```

For detailed API documentation:
- [**Converter API Guide**](https://clickin.github.io/stax-xml): Declarative parsing with schemas
- [**StaxXmlParser (Asynchronous)**](https://clickin.github.io/stax-xml): Event-based parsing from streams
- [**StaxXmlParserSync (Synchronous)**](https://clickin.github.io/stax-xml): Event-based parsing from strings
- **StaxXmlCursorReaderSync (Synchronous Cursor)**: Cursor-style pull reader from strings

### 🌐 Platform Compatibility

Expand Down
23 changes: 23 additions & 0 deletions packages/stax-xml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,33 @@ for (const event of parser) {
}
```


##### Cursor-style Synchronous Parsing (StaxXmlCursorReaderSync)

```typescript
import { StaxXmlCursorReaderSync, XmlEventType } from 'stax-xml';

const reader = new StaxXmlCursorReaderSync('<root id="1"><item>text</item></root>');

while (reader.read()) {
const event = reader.requireEvent();

if (event.type === XmlEventType.START_ELEMENT) {
console.log(event.name, event.getAttributeCount());
console.log(event.getAttributeValue('id'));
}

if (event.type === XmlEventType.CHARACTERS) {
console.log(event.text);
}
}
```

For detailed API documentation:
- [**Converter API Guide**](https://clickin.github.io/stax-xml): Declarative parsing with schemas
- [**StaxXmlParser (Asynchronous)**](https://clickin.github.io/stax-xml): Event-based parsing from streams
- [**StaxXmlParserSync (Synchronous)**](https://clickin.github.io/stax-xml): Event-based parsing from strings
- **StaxXmlCursorReaderSync (Synchronous Cursor)**: Cursor-style pull reader from strings

### 🌐 Platform Compatibility

Expand Down
64 changes: 64 additions & 0 deletions packages/stax-xml/src/AttrStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AttributeInfo, StartElementEvent } from './types';
import { CursorAttribute } from './types';

function parseAttributeName(name: string): Pick<CursorAttribute, 'localName' | 'prefix'> {
const colonIndex = name.indexOf(':');
if (colonIndex === -1) {
return { localName: name, prefix: undefined };
}

return {
prefix: name.slice(0, colonIndex),
localName: name.slice(colonIndex + 1)
};
}

export class AttrStore {
private readonly list: CursorAttribute[];

constructor(startEvent: StartElementEvent) {
this.list = AttrStore.fromEvent(startEvent);
}

get count(): number {
return this.list.length;
}

getByIndex(index: number): CursorAttribute | undefined {
return this.list[index];
}

getByName(name: string): CursorAttribute | undefined {
return this.list.find((attr) => attr.name === name);
}

toArray(): CursorAttribute[] {
return this.list;
}

private static fromEvent(event: StartElementEvent): CursorAttribute[] {
if (event.attributesWithPrefix) {
return Object.entries(event.attributesWithPrefix).map(([name, info]) => {
const typedInfo = info as AttributeInfo;
return {
name,
localName: typedInfo.localName,
prefix: typedInfo.prefix,
Comment on lines +44 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive cursor attribute fields from qualified name

When AttrStore.fromEvent reads event.attributesWithPrefix, it assumes each value already contains localName and that the entry key is the full attribute name. For StartElementEvents produced by the existing async parser, the map is keyed by local name and values do not include localName, so XmlCursorEvent ends up with CursorAttribute.localName as undefined and drops prefixes from name (for example, a:id becomes id), which also breaks getAttributeValue('a:id') lookups when wrapping those events.

Useful? React with 👍 / 👎.

uri: typedInfo.uri,
value: typedInfo.value
} satisfies CursorAttribute;
});
}

return Object.entries(event.attributes).map(([name, value]) => {
const parsedName = parseAttributeName(name);
return {
name,
localName: parsedName.localName,
prefix: parsedName.prefix,
uri: undefined,
value
} satisfies CursorAttribute;
});
}
}
67 changes: 67 additions & 0 deletions packages/stax-xml/src/StaxXmlCursorReaderSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { StaxXmlParserSync, StaxXmlParserSyncOptions } from './StaxXmlParserSync';
import { AnyXmlEvent, XmlCursorReaderSyncLike } from './types';
import { XmlCursorEvent } from './XmlCursorEvent';

export interface StaxXmlCursorReaderSyncOptions extends StaxXmlParserSyncOptions {}

export class StaxXmlCursorReaderSync implements XmlCursorReaderSyncLike {
private readonly iterator: Iterator<AnyXmlEvent>;
private buffer: IteratorResult<AnyXmlEvent> | null = null;
private finished: boolean = false;
private current: XmlCursorEvent | null = null;

constructor(xml: string, options: StaxXmlCursorReaderSyncOptions = {}) {
this.iterator = new StaxXmlParserSync(xml, options);
}

hasNext(): boolean {
if (this.finished) {
return false;
}

if (this.buffer !== null) {
return !this.buffer.done;
}

this.buffer = this.iterator.next();
return !this.buffer.done;
}

read(): boolean {
if (this.finished) {
this.current = null;
return false;
}

const result = this.buffer ?? this.iterator.next();
this.buffer = null;

if (result.done) {
this.finished = true;
this.current = null;
return false;
}

this.current = new XmlCursorEvent(result.value);
return true;
}

getEvent(): XmlCursorEvent | null {
return this.current;
}

requireEvent(): XmlCursorEvent {
if (!this.current) {
throw new Error('No active cursor event. Call read() first.');
}

return this.current;
}
}

export function createStaxXmlCursorReaderSync(
xml: string,
options: StaxXmlCursorReaderSyncOptions = {}
): StaxXmlCursorReaderSync {
return new StaxXmlCursorReaderSync(xml, options);
}
85 changes: 85 additions & 0 deletions packages/stax-xml/src/XmlCursorEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AttrStore } from './AttrStore';
import {
AnyXmlEvent,
CursorAttribute,
CursorXmlEventType,
StartElementEvent,
XmlEventType,
isStartElement
} from './types';

export class XmlCursorEvent {
private readonly attrStore: AttrStore | null;

constructor(private readonly raw: AnyXmlEvent) {
this.attrStore = isStartElement(raw) ? new AttrStore(raw as StartElementEvent) : null;
}

get type(): CursorXmlEventType {
return this.raw.type;
}

get name(): string | undefined {
return 'name' in this.raw ? this.raw.name : undefined;
}

get localName(): string | undefined {
return 'localName' in this.raw ? this.raw.localName : undefined;
}

get prefix(): string | undefined {
return 'prefix' in this.raw ? this.raw.prefix : undefined;
}

get uri(): string | undefined {
return 'uri' in this.raw ? this.raw.uri : undefined;
}

get text(): string | undefined {
return 'value' in this.raw ? this.raw.value : undefined;
}

get error(): Error | undefined {
return 'error' in this.raw ? this.raw.error : undefined;
}

isStartElement(): boolean {
return this.type === XmlEventType.START_ELEMENT;
}

isEndElement(): boolean {
return this.type === XmlEventType.END_ELEMENT;
}

isCharacters(): boolean {
return this.type === XmlEventType.CHARACTERS;
}

isCdata(): boolean {
return this.type === XmlEventType.CDATA;
}

isStartDocument(): boolean {
return this.type === XmlEventType.START_DOCUMENT;
}

isEndDocument(): boolean {
return this.type === XmlEventType.END_DOCUMENT;
}

getAttributeCount(): number {
return this.attrStore?.count ?? 0;
}

getAttribute(index: number): CursorAttribute | undefined {
return this.attrStore?.getByIndex(index);
}

getAttributeValue(name: string): string | undefined {
return this.attrStore?.getByName(name)?.value;
}

toEvent(): AnyXmlEvent {
return this.raw;
}
}
6 changes: 4 additions & 2 deletions packages/stax-xml/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export * from "./StaxXmlParser.js";
export * from "./StaxXmlParserSync.js";
export * from "./StaxXmlCursorReaderSync.js";
export * from "./StaxXmlWriter.js";
export * from "./StaxXmlWriterSync.js";
export * from "./XmlCursorEvent.js";

export { isCdata, isCharacters, isEndDocument, isEndElement, isError, isStartDocument, isStartElement, XmlEventType } from "./types.js";
export type {
AnyXmlEvent, CdataEvent, CharactersEvent, ErrorEvent, StartElementEvent, WriteElementOptions, XmlAttribute
AnyXmlEvent, CdataEvent, CharactersEvent, CursorAttribute, CursorXmlEventType, ErrorEvent, StartElementEvent, WriteElementOptions, XmlAttribute,
XmlCursorEventLike, XmlCursorReaderSyncLike
} from "./types.js";

52 changes: 51 additions & 1 deletion packages/stax-xml/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,56 @@ export type AnyXmlEvent =
| CdataEvent
| ErrorEvent;

/**
* Cursor-compatible XML event type.
*
* @public
*/
export type CursorXmlEventType = XmlEventType;

/**
* Attribute shape used by the cursor API.
*
* @public
*/
export interface CursorAttribute {
name: string;
localName: string;
prefix?: string;
uri?: string;
value: string;
}

/**
* Read-only view for cursor events.
*
* @public
*/
export interface XmlCursorEventLike {
readonly type: CursorXmlEventType;
readonly name: string | undefined;
readonly localName: string | undefined;
readonly prefix: string | undefined;
readonly uri: string | undefined;
readonly text: string | undefined;
readonly error: Error | undefined;
getAttributeCount(): number;
getAttribute(index: number): CursorAttribute | undefined;
getAttributeValue(name: string): string | undefined;
}

/**
* Synchronous cursor-style reader API.
*
* @public
*/
export interface XmlCursorReaderSyncLike {
hasNext(): boolean;
read(): boolean;
getEvent(): XmlCursorEventLike | null;
requireEvent(): XmlCursorEventLike;
}

/**
* Attribute interface (for Writer)
*/
Expand Down Expand Up @@ -357,4 +407,4 @@ export interface WriteElementOptions {
attributes?: Record<string, string | AttributeInfo>;
selfClosing?: boolean;
comment?: string;
}
}
Loading