Open-source project
gajus/surgeon avatar
gajus/surgeon

Surgeon: describing DOM extraction as a string expression

Declarative DOM extraction expression evaluator. 👨‍⚕️

687 stars30 forksJavaScriptNOASSERTION

At a glance

What is it?
Surgeon is a JavaScript library that turns DOM extraction into a declarative string expression, with subroutines for selecting, reading, formatting and testing, a pipe operator for composition, and pluggable evaluators for the browser and for Node. It has not been released since 2020.
Who is it for?
Surgeon suits a team that wants extraction rules written once in a readable, testable, declarative form and run in either a browser or Node, and it is a good fit for rule sets that a domain expert rather than a programmer has to maintain. It is the wrong choice for a new project, because the newest tagged release is v3.16.4 from 2020 and the last push was on 2026-07-08, and because the package still declares Node 10 as its engine floor.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 109 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Extraction rules as data, not as code

Surgeon describes itself as a declarative DOM extraction expression evaluator, and the promise is that extraction becomes a value you can hold, store, transmit and test, rather than a chain of method calls you have to read to understand. The example in the README is a YAML document, which is the clearest statement of the idea. A named result such as `imageUrl` is a list of commands, and the commands are strings:

yaml
articles:
- select article {0,}
- body:
  - select .body
  - read property innerHTML
  imageUrl:
  - select img
  - read attribute src
  summary:
  - select ".body p:first-child"
  - read property innerHTML
  - format text
  title:
  - select .title
  - read property textContent
pageName:
- select .body
- read property innerHTML

Each key is a field of the extracted record, and each list is a sequence of operations. The claim in the README is a powerful, succinct, composable, extendable, declarative API, and the practical payoff is that a rule set can live in a configuration file next to the code that consumes it, or in a database, or travel over the network.

Subroutines are the execution model

The evaluator advances through what the documentation calls subroutines, and the term is used to emphasise the cross platform nature of the declarative API. A subroutine is a function used to advance the extraction, and the built-in form is a string with an optional argument. The README gives the shape as an expression with two subroutines where `foo` takes no value and `bar` takes one, `baz`:

js
x('foo | bar baz', 'qux');

The first subroutine receives the document input, in this case the `qux` string, and the result of each subroutine is passed to the next. Subroutines run in the order they are defined, and the array form is equivalent to the piped string form, which the documentation shows as `x(['foo', 'bar baz'], 'qux')`. The built-in list covers `append`, `closest`, `constant`, `format`, `match`, `nextUntil`, `prepend`, `previous`, `read`, `remove`, `select` and `test`, and each has a documented parameter table. User-defined subroutines are the extension point, alongside inline subroutines, and built-in subroutine aliases exist for the common ones.

The pipe operator shortens the common case

The documentation anticipates the objection that the long form is verbose, and points to aliases and the pipe operator as the answer. The README shows the same extraction in the compact form, where `s` is a select alias, `rp` is read property, `ra` is read attribute and `f` is format, and the pipe operator concatenates the commands:

The `sm` alias covers the select quantifier syntax, where `article {0,}` in the long form means select article with a quantifier of zero or more. The compact version of the same document reads `pageName: s .body | rp innerHTML` for the last field and `body: s .body | rp innerHTML` for the body, with `imageUrl: s img | ra src` and `summary: s .body p:first-child | rp innerHTML | f text` alongside. The two forms are equivalent, which is the important point, since the long form stays readable for someone who does not know the aliases and the short form stays fast to type for someone who does. Aliases are declared in the configuration, and the built-in ones are a separate mechanism from user-defined subroutines.

Two evaluators, one expression

Subroutines do not parse the document themselves. They use an evaluator, which is the HTML parser and selector engine, and the default depends on the environment. The browser evaluator is used if `window` and `document` variables are defined, and the cheerio evaluator is used otherwise, which in practice means Node. You can also select one explicitly, and the README shows both shapes. For the browser case, which is what you would use in a headless browser as well as a real one, the evaluator is imported and passed to the entry function:

js
import {
  browserEvaluator
} from './evaluators';

surgeon({
  evaluator: browserEvaluator()
});

The cheerio variant is identical apart from the import, and it is the one to use in Node. The evaluator implementations live in `src/evaluators/browserEvaluator.js` and `src/evaluators/cheerioEvaluator.js`, and the documentation says that if you need a different evaluator you should raise an issue, with those two files as the reference implementations. That is a small surface, but it is the seam where a different DOM implementation would be plugged in.

What the dependencies reveal about the design

The package metadata is informative. The runtime dependencies are `cheerio` at 1.0.0-rc.3 or later, which is the Node DOM implementation, `es6-error` for typed errors, `pianola`, `regex-parser` for the regular expression handling the `match` and `test` subroutines need, and `roarr` for structured logging. The `regex-parser` dependency in particular explains why `match` and `test` can take a pattern and why the documentation talks about validating results with RegExp and with a user-defined test function. The keyword list is css, declarative, dom, parser and selector, and the main entry point is `./dist/index.js`. The build is Babel based, with Flow type stripping in the toolchain, ESLint using the canonical config, AVA for the tests, nyc for coverage, husky for the pre-commit hook, and semantic-release for versioning, which is why the tag names look the way they do. The engines field says Node 10 or newer.

The gap between the releases and the commits

The release history is the first thing to check on this project, and it is mixed. The recent tags are v3.16.4, v3.16.3 and v3.16.2, all published on the same day in June 2020, which is the signature of a patch release train rather than a steady release cadence. The last push was on 2026-07-08, so work has continued in the repository without producing new tags. The gap matters in practice. The version of the package you install from npm is the 2020 one, while the repository may contain newer work, and the two are not the same artefact. The semantic-release configuration suggests the release process was intended to be automated, and the fact that it is not producing tags is a question worth asking before you depend on the project.

Where Surgeon does not reach

Three limits are clear. The first is scope. The built-in subroutine set is fixed and modest, covering selection, reading, formatting, appending, traversal and testing, and everything else, including navigating complex structures, has to be written as a user-defined subroutine, which means writing JavaScript and therefore losing the declarative property that made the library interesting. The second is the coupling to cheerio's versioning: the dependency is pinned to a 1.0.0 release candidate line; a Node project that must keep its dependencies current has a friction point that the library does not resolve. The third is the age of the published artefact, which pushes the question back to whether a fresh implementation over cheerio or a headless browser would serve you better. What Surgeon still does well is the thing it was built for, which is a shared, readable rule language for extraction that non-programmers can be handed.

Editorial conclusion

Surgeon suits a team that wants extraction rules written once in a readable, testable, declarative form and run in either a browser or Node, and it is a good fit for rule sets that a domain expert rather than a programmer has to maintain. It is the wrong choice for a new project, because the newest tagged release is v3.16.4 from 2020 and the last push was on 2026-07-08, and because the package still declares Node 10 as its engine floor. Before you adopt it, check whether the subroutine set covers the operations you need, since the built-in list is fixed and extension means writing your own, and read the error handling and debugging sections before relying on it in a pipeline. The licence is BSD-3-Clause in the package metadata, while repository metadata lists the licence as unclassifiable.

Frequently asked questions

What is Surgeon used for?

Surgeon is a declarative DOM extraction expression evaluator. You describe an extraction as a list of subroutines, such as `select` and `read property innerHTML`, and it returns the extracted values, which lets extraction rules live in configuration rather than in code.

Which evaluator does Surgeon use in Node.js?

The cheerio evaluator, since the browser evaluator is only selected when `window` and `document` are defined. You can also select it explicitly by importing `cheerioEvaluator` and passing it to the entry function.

How do I extend Surgeon with my own operations?

Through user-defined subroutines, which are configured alongside the built-in ones, plus inline subroutines and aliases for the built-ins. The built-in set includes append, closest, constant, format, match, nextUntil, prepend, previous, read, remove, select and test.

Is Surgeon still maintained?

The last push was on 2026-07-08, but the most recent tagged release is v3.16.4 from 2020-06-05, and the package metadata still declares Node 10 as the supported engine. Treat the published package as frozen and check the repository before adopting it.

Official sources

  1. gajus/surgeon on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gajus-surgeon.svg)](https://hysenlabs.com/projects/gajus-surgeon)

Was this useful?