gff-nostream
v5.5.0
Published
utilities to read GFF3 data
Maintainers
Readme
gff-nostream
Parse GFF3 data. A simplified version of @gmod/gff with no Node.js stream dependency.
Install
pnpm add gff-nostreamUsage
import { readFileSync } from 'node:fs'
import { parseStringSync } from 'gff-nostream'
const features = parseStringSync(readFileSync('my_annotations.gff3', 'utf8'))There is no filesystem dependency — in the browser, pass any GFF3 string, such
as one from fetch(…).then(r => r.text()). If your GFF3 is a remote .gff3.gz,
query it with @gmod/tabix, reading through a
filehandle from
@gmod/range-cache-filehandle
so that its byte ranges are cached, and parse the lines it hands back with the
functions here.
Object format
The parser hands back flat objects: coordinates converted to 0-based half-open,
strand as a number (1/-1/0), attributes spread as lowercase top-level
keys, single-valued attributes unwrapped from their array, and child features
nested under subfeatures.
A gene with an mRNA child:
{
"refName": "ctg123",
"source": null,
"type": "gene",
"start": 999,
"end": 9000,
"strand": 1,
"subfeatures": [
{
"refName": "ctg123",
"source": null,
"type": "mRNA",
"start": 1049,
"end": 9000,
"strand": 1,
"subfeatures": [],
"id": "mRNA00001",
"parent": "gene00001",
"name": "EDEN.1"
}
],
"id": "gene00001",
"name": "EDEN"
}The fixed fields are refName, source, type, start, end, score,
strand, phase, and subfeatures. The parser appends 2 to an attribute
whose lowercased name matches one of them — Start= becomes start2. It
reserves seq_id and refname the same way, so a Seq_id= attribute becomes
seq_id2 and is never stored beside refName.
Parsing behavior
These apply to every parse function below.
The parser ignores comments, directives, and ##FASTA sections.
Attribute tags are matched with surrounding spaces trimmed, so Name=A; ID=x
carries an ID, as it does under @gmod/gff.
A multi-location feature — the same ID on several lines, such as a CDS spanning
several segments — stays one line per segment under its parent: each line
attaches to the parent as its own flat feature. With no parent, the first line
becomes the feature, spanning every segment, and each segment — the first
included — hangs under it as a subfeature of the same type. An NCBI
cDNA_match, written one line per aligned block, therefore comes back as one
feature carrying its blocks, and a single-line feature gains no child. A
repeated ID whose first line already has children, or whose lines differ in
type, is a duplicate rather than one feature, and each line stays its own
top-level item.
A feature whose Parent never appears in the input comes back as a top-level
feature, after the ones that did, rather than dropping. This happens routinely
when parsing a slice of a file, e.g. a tabix region query that cuts off the
parent line.
Lazy parsing
Every parse function has a …Lazy counterpart that leaves column 9 as raw text
on feature.attributeString instead of spreading it into keys. It reads only
ID and Parent, since it cannot build the tree without them.
import { getAttribute, parseLinesLazy } from 'gff-nostream'
const features = parseLinesLazy(lines)
const names = features.map(f => getAttribute(f, 'name'))Lazy parsing is worth it when most attributes are never read, and not when they
are, because each getAttribute rescans the string. See
docs/lazy-parsing.md for the measured trade-off and the
gotchas.
API
Parsing
parseStringSync(str: string): GffFeature[]
Parse a GFF3 string.
parseLines(lines: readonly string[]): GffFeature[]
Parse an array of raw GFF3 feature lines, for a caller that has split and
filtered the file itself — a tabix region query, or a whole-file scan grouping
lines by reference sequence. The lines must already be free of blanks, comments,
and any ##FASTA section.
parseRecords<R extends LineRecord>(records: readonly R[]): ParsedRecord<R>[]
Parse an array of records wrapping raw GFF3 lines. Each top-level feature comes
back paired with the record it came from, so a caller can attach its own stable
id (a byte offset, a hash, …) without the parser stamping anything onto the
feature. Records may carry extra fields (R is inferred), which pass through
untouched on record.
const records = lines.map((line, i) => ({ line, offset: offsets[i] }))
const features = parseRecords(records).map(({ feature, record }) => ({
...feature,
id: record.offset,
}))parseLinesLazy / parseRecordsLazy
parseLines and parseRecords returning LazyGffFeatures. See
Lazy parsing.
parseFeatureLazy(line: string): LazyGffFeature
Parse a single line, with no tree building.
Attributes
getAttribute(feature: LazyGffFeature, key: string): unknown
The value of one attribute, by its parsed key (lowercased, 2-suffixed if
reserved). A single-valued attribute comes back as a string, a multi-valued one
as a string array, and an absent one as undefined — the same shapes the eager
parser produces.
getAttributes(feature: LazyGffFeature): Record<string, unknown>
Every attribute, as the eager parser would have spread them onto the feature.
getLinkAttributes(feature: LazyGffFeature): { id: unknown; parent: unknown }
ID and Parent in one pass. The lazy parsers build the tree with it, and the
package exports it for callers doing their own linking.
Other
extractType(line: string): string
Extract the feature type (GFF3 column 3) from a raw line without fully splitting
it. Returns '' for a line with fewer than two tabs.
hasIdAttribute(line: string): boolean
Whether a raw line carries an ID the parser would link children to. A record
with no ID can be named by no Parent=, so this is the exact test for whether
a line can have children, and it uses the parser's own tag scan, so it agrees
with the linker about spacing and case. A region reader that widens its fetch to
complete a record's subfeature list can bound the widening with it.
Types
interface LineRecord {
line: string
}
interface ParsedRecord<R extends LineRecord = LineRecord> {
feature: GffFeature
record: R // the input record this top-level feature came from
}ParsedLazyRecord is ParsedRecord with a LazyGffFeature. The package also
exports GffFeature and LazyGffFeature.
Contributing
See CONTRIBUTING.md for development and release steps.
