├── src ├── compat.js ├── lib.js ├── node-libtidy.hh ├── node-libtidy.cc ├── opt.hh ├── worker.hh ├── buf.hh ├── htmltidy.js ├── memory.hh ├── doc.hh ├── util.hh ├── TidyDoc.js ├── worker.cc ├── memory.cc ├── index.d.ts ├── cli.js ├── index.js ├── opt.cc └── doc.cc ├── .gitignore ├── .gitmodules ├── .pre-commit-hooks.yaml ├── parse-version.js ├── release.md ├── appveyor.yml ├── test ├── htmltidy-test.js ├── hi-test.js ├── ts-decl-test.ts ├── opt-test.js └── doc-test.js ├── LICENSE.md ├── .travis.yml ├── util └── gen-typescript-decl.js ├── package.json ├── binding.gyp ├── README.md └── API.md /src/compat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.htmltidy = require("./htmltidy"); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /lib/ 3 | /src/options.d.ts 4 | /node_modules/ 5 | .gdb_history 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tidy-html5"] 2 | path = tidy-html5 3 | url = https://github.com/htacg/tidy-html5.git 4 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = require( 4 | require("node-pre-gyp").find(require.resolve("../package.json"))); 5 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: htmltidy 2 | name: HTML Tidy 3 | description: Tidies HTML files 4 | language: node 5 | types: [html] 6 | entry: htmltidy -modify -- 7 | -------------------------------------------------------------------------------- /src/node-libtidy.hh: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | extern "C" { 6 | #include 7 | #include 8 | } 9 | 10 | #include "util.hh" 11 | #include "memory.hh" 12 | #include "buf.hh" 13 | #include "opt.hh" 14 | #include "doc.hh" 15 | #include "worker.hh" 16 | -------------------------------------------------------------------------------- /src/node-libtidy.cc: -------------------------------------------------------------------------------- 1 | #include "node-libtidy.hh" 2 | 3 | NAN_MODULE_INIT(Init) { 4 | node_libtidy::initMemory(); 5 | node_libtidy::Opt::Init(target); 6 | node_libtidy::Doc::Init(target); 7 | Nan::Set(target, Nan::New("libraryVersion").ToLocalChecked(), 8 | Nan::New(tidyLibraryVersion()).ToLocalChecked()); 9 | } 10 | NODE_MODULE(libtidy, Init) 11 | -------------------------------------------------------------------------------- /parse-version.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | 5 | var path = require.resolve("./tidy-html5/version.txt"); 6 | var content = fs.readFileSync(path, "utf-8").split(/\r?\n/g); 7 | 8 | switch(process.argv[2]) { 9 | case "version": 10 | console.log(content[0]); 11 | break; 12 | case "date": 13 | console.log(content[1].replace(/\./g, "/")); 14 | break; 15 | default: 16 | throw Error('Must specify argument "version" or "date"'); 17 | } 18 | -------------------------------------------------------------------------------- /src/opt.hh: -------------------------------------------------------------------------------- 1 | namespace node_libtidy { 2 | 3 | namespace Opt { 4 | 5 | extern Nan::Persistent constructor; 6 | NAN_MODULE_INIT(Init); 7 | NAN_METHOD(New); 8 | v8::Local Create(TidyOption opt); 9 | TidyOption Unwrap(v8::Local object); 10 | 11 | NAN_METHOD(toString); 12 | NAN_PROPERTY_GETTER(getCategory); 13 | NAN_PROPERTY_GETTER(getDefault); 14 | NAN_PROPERTY_GETTER(getId); 15 | NAN_PROPERTY_GETTER(getName); 16 | NAN_PROPERTY_GETTER(getPickList); 17 | NAN_PROPERTY_GETTER(getReadOnly); 18 | NAN_PROPERTY_GETTER(getType); 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/worker.hh: -------------------------------------------------------------------------------- 1 | namespace node_libtidy { 2 | 3 | class TidyWorker : public Nan::AsyncWorker { 4 | public: 5 | TidyWorker(Doc* doc, 6 | v8::Local resove, 7 | v8::Local reject); 8 | void setInput(const char* data, size_t length); 9 | void Execute(); 10 | void WorkComplete(); 11 | 12 | bool shouldCleanAndRepair; 13 | bool shouldRunDiagnostics; 14 | bool shouldSaveToBuffer; 15 | 16 | private: 17 | WorkerParent parent; 18 | Doc* doc; 19 | TidyBuffer input; 20 | Buf output; 21 | int rc; 22 | const char* lastFunction; 23 | Nan::Callback resolve; 24 | Nan::Callback reject; 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # How to create a release 2 | 3 | 1. Clean tree: `git clean -idx`. 4 | 1. Rebuild library locally: `npm install`. 5 | 1. Run tests: `npm test`. 6 | 1. Examine commit history since last release to identify relevant changes. 7 | 1. Create a signed and annotated git tag: `git tag -s vX.Y.Z`. 8 | Include a short description of the relevant changes. 9 | 1. Publish it to GitHub: `git push origin tag vX.Y.Z`. 10 | 1. Wait for Travis and AppVeyor to build precompiled libraries for it. 11 | These should get uploaded to a release draft automatically. 12 | 1. Edit release draft on GitHub and publish it. 13 | Change name of release from vX.Y.Z to X.Y.Z. 14 | Include description of changes, possibly with improved Markdown formatting. 15 | 1. Publish release on NPM: `npm publish` 16 | -------------------------------------------------------------------------------- /src/buf.hh: -------------------------------------------------------------------------------- 1 | 2 | namespace node_libtidy { 3 | 4 | class Buf { 5 | 6 | public: 7 | 8 | Buf() { tidyBufInitWithAllocator(&buf, &allocator); } 9 | 10 | ~Buf() { 11 | tidyBufFree(&buf); 12 | } 13 | 14 | operator TidyBuffer*() { 15 | return &buf; 16 | } 17 | 18 | bool isEmpty() { 19 | return buf.size == 0; 20 | } 21 | 22 | void reset() { 23 | buf.next = buf.size = 0; 24 | } 25 | 26 | v8::MaybeLocal string() const { 27 | return Nan::New(data(), buf.size); 28 | } 29 | 30 | v8::MaybeLocal buffer() const { 31 | return Nan::CopyBuffer(data(), buf.size); 32 | } 33 | 34 | private: 35 | 36 | TidyBuffer buf; 37 | 38 | char* data() const { 39 | return b2c(buf.bp); 40 | } 41 | 42 | friend std::ostream& operator<<(std::ostream& out, Buf& buf); 43 | 44 | }; 45 | 46 | inline std::ostream& operator<<(std::ostream& out, Buf& buf) { 47 | return out.write(buf.data(), buf.buf.size); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/htmltidy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Interface like htmltidy, see https://www.npmjs.com/package/htmltidy 4 | 5 | var TidyDoc = require("./TidyDoc"); 6 | 7 | module.exports.tidy = function(text, opts, cb) { 8 | if (typeof cb === "undefined" && typeof opts === "function") { 9 | cb = opts; 10 | opts = {}; 11 | } 12 | if (typeof cb !== "function") { 13 | throw Error("no callback provided"); 14 | } 15 | opts = opts || {}; 16 | var doc = TidyDoc(); 17 | doc.options = { // magic setter 18 | show_warnings: false, 19 | tidy_mark: false, 20 | force_output: true, 21 | quiet: false, 22 | }; 23 | doc.options = opts; // another magic setter 24 | if (!Buffer.isBuffer(text)) 25 | text = Buffer(String(text)); 26 | doc._async2(text, true, true, true, function(res) { 27 | var errlog = res.errlog; 28 | var output = res.output; 29 | if (Buffer.isBuffer(output)) 30 | output = output.toString(); 31 | if (!doc.optGet("show-warnings")) 32 | errlog = ""; 33 | cb(errlog, output); 34 | }, cb); 35 | }; 36 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | github_release_token: 3 | secure: TTQbwwwftUsijup2DaIPSnfBoYtXNrPVUFxNdFF/kugUndKeQk2k7YUSe1C1ojZn 4 | matrix: 5 | - nodejs_version: 5 6 | - nodejs_version: 6 7 | - nodejs_version: 7 8 | - nodejs_version: 8 9 | - nodejs_version: 9 10 | 11 | platform: 12 | - x86 13 | - x64 14 | 15 | image: Visual Studio 2015 16 | 17 | install: 18 | - ps: Install-Product node $env:nodejs_version $env:platform 19 | - npm install -g npm 20 | - node --version 21 | - npm --version 22 | - git submodule update --init --recursive 23 | 24 | build_script: 25 | - npm install --build-from-source 26 | 27 | test_script: 28 | - npm test 29 | 30 | after_test: 31 | - node_modules/.bin/node-pre-gyp package 32 | 33 | artifacts: 34 | - path: build\stage\gagern\node-libtidy\releases\download\**\*.tar.gz 35 | name: binary 36 | 37 | deploy: 38 | - provider: GitHub 39 | release: $(APPVEYOR_REPO_TAG_NAME) 40 | artifact: binary 41 | auth_token: $(github_release_token) 42 | draft: true 43 | prerelease: false 44 | on: 45 | appveyor_repo_tag: true 46 | -------------------------------------------------------------------------------- /src/memory.hh: -------------------------------------------------------------------------------- 1 | namespace node_libtidy { 2 | 3 | extern TidyAllocator allocator; 4 | 5 | void adjustMem(ssize_t diff); 6 | 7 | // An object of the following class must be created on the main V8 thread 8 | // and be kept alive during the execution of a worker thread, 9 | // to be eventually destroyed on the main V8 thread again. 10 | // Only a single worker is allowed per parent. 11 | class TIDY_EXPORT WorkerParent { 12 | public: 13 | WorkerParent(); 14 | virtual ~WorkerParent(); 15 | private: 16 | friend void adjustMem(ssize_t); 17 | ssize_t memAdjustments; 18 | }; 19 | 20 | // An object of the following class must be created in the worker thread, 21 | // and kept alive as long as the worker interfaces with libxmljs. 22 | // It must eventually be destroyed while still in the worker thread. 23 | class TIDY_EXPORT WorkerSentinel { 24 | public: 25 | WorkerSentinel(WorkerParent& parent); 26 | virtual ~WorkerSentinel(); 27 | private: 28 | friend void adjustMem(ssize_t); 29 | WorkerParent& parent; 30 | }; 31 | 32 | void initMemory(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /test/htmltidy-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | chai.use(require("chai-subset")); 5 | var expect = chai.expect; 6 | 7 | // Can't use htmltidy 0.0.6 (the “original”) due to 8 | // https://github.com/vavere/htmltidy/issues/24 9 | var fork = require("htmltidy2"); 10 | var clone = require("../").compat.htmltidy; 11 | 12 | describe("htmltidy interface:", function() { 13 | 14 | describe("tidy function:", function() { 15 | 16 | it("Handles a simple document the same way", function(done) { 17 | var ores = null, cres = null; 18 | var doc = 'test

Body'; 19 | fork.tidy(doc, {}, function(err, doc) { 20 | ores = {err: err, doc: doc}; 21 | if (cres) process.nextTick(compare); 22 | }); 23 | clone.tidy(doc, {}, function(err, doc) { 24 | cres = {err: err, doc: doc}; 25 | if (ores) process.nextTick(compare); 26 | }); 27 | function compare() { 28 | expect(cres.err).to.be.equal(ores.err); 29 | expect(cres.doc).to.be.equal(ores.doc); 30 | done(); 31 | } 32 | }); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin von Gagern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/doc.hh: -------------------------------------------------------------------------------- 1 | namespace node_libtidy { 2 | 3 | class Doc : public Nan::ObjectWrap { 4 | public: 5 | Doc(); 6 | ~Doc(); 7 | 8 | TidyOption asOption(v8::Local value); 9 | bool CheckResult(int rc, const char* functionName); 10 | v8::Local exception(int rc); 11 | void Lock() { locked = true; } 12 | void Unlock() { locked = false; } 13 | 14 | static NAN_MODULE_INIT(Init); 15 | 16 | private: 17 | TidyDoc doc; 18 | Buf err; 19 | bool locked; 20 | 21 | static Doc* Prelude(v8::Local self); 22 | 23 | static NAN_METHOD(New); 24 | static NAN_METHOD(parseBufferSync); 25 | static NAN_METHOD(cleanAndRepairSync); 26 | static NAN_METHOD(runDiagnosticsSync); 27 | static NAN_METHOD(saveBufferSync); 28 | static NAN_METHOD(getOptionList); 29 | static NAN_METHOD(getOption); 30 | static NAN_METHOD(optGet); 31 | static NAN_METHOD(optSet); 32 | static NAN_METHOD(optGetCurrPick); 33 | static NAN_METHOD(optGetDoc); 34 | static NAN_METHOD(optGetDocLinksList); 35 | static NAN_METHOD(optResetToDefault); 36 | static NAN_METHOD(async); 37 | static NAN_METHOD(getErrorLog); 38 | 39 | static Nan::Persistent constructor; 40 | 41 | friend class TidyWorker; 42 | }; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/util.hh: -------------------------------------------------------------------------------- 1 | namespace node_libtidy { 2 | 3 | inline void 4 | SetAccessor(v8::Local&, 5 | v8::Local& otpl, 6 | const char* name, 7 | Nan::GetterCallback getter) { 8 | Nan::SetAccessor(otpl, Nan::New(name).ToLocalChecked(), getter); 9 | } 10 | 11 | inline const byte* c2b(const char* c) { 12 | return reinterpret_cast(c); 13 | } 14 | 15 | inline byte* c2b(char* c) { 16 | return reinterpret_cast(c); 17 | } 18 | 19 | inline const char* b2c(const byte* b) { 20 | return reinterpret_cast(b); 21 | } 22 | 23 | inline char* b2c(byte* b) { 24 | return reinterpret_cast(b); 25 | } 26 | 27 | inline Bool bb(bool b) { 28 | return b ? yes : no; 29 | } 30 | 31 | inline bool bb(Bool b) { 32 | return b != no; 33 | } 34 | 35 | inline std::ostream& operator<<(std::ostream& out, 36 | const Nan::Utf8String& str) { 37 | return out.write(*str, str.length()); 38 | } 39 | 40 | inline v8::Local NewString(const std::string& str) { 41 | return Nan::New(str.c_str(), str.length()).ToLocalChecked(); 42 | } 43 | 44 | inline std::string trim(std::string str) { 45 | while (str.length() && str[str.length() - 1] == '\n') 46 | str.resize(str.length() - 1); 47 | return str; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /test/hi-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | chai.use(require("chai-subset")); 5 | var expect = chai.expect; 6 | var util = require("util"); 7 | var libtidy = require("../"); 8 | 9 | describe("High-level API:", function() { 10 | 11 | var testDoc1 = Buffer('\n\n' + 12 | '

foo

'); 13 | 14 | describe("tidyBuffer:", function() { 15 | 16 | it("on simple document", function(done) { 17 | libtidy.tidyBuffer(testDoc1, function(err, res) { 18 | expect(err).to.be.null; 19 | expect(res).to.contain.key("output"); 20 | expect(res).to.contain.key("errlog"); 21 | expect(res.errlog).to.match(/inserting missing/); 22 | expect(res.errlog).to.match(/looks like HTML5/); 23 | expect(res.errlog).to.match(/Tidy found/); 24 | expect(Buffer.isBuffer(res.output)).ok; 25 | expect(res.output.toString()).to.match(/.*<\/title>/); 26 | done(); 27 | }); 28 | }); 29 | 30 | it("doesn't use CRLF in its output", function(done) { 31 | libtidy.tidyBuffer(testDoc1, function(err, res) { 32 | expect(res.output.toString()).to.not.match(/\r\n/); 33 | done(); 34 | }); 35 | }); 36 | 37 | it("Converts argument to buffer", function(done) { 38 | libtidy.tidyBuffer({ 39 | toString: testDoc1.toString.bind(testDoc1) 40 | }, function(err, res) { 41 | expect(err).to.be.null; 42 | expect(Buffer.isBuffer(res.output)).ok; 43 | expect(res.output.toString()).to.match(/<title>.*<\/title>/); 44 | done(); 45 | }); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | - osx 5 | node_js: 6 | - "4" 7 | - "5" 8 | - "6" 9 | - "7" 10 | - "8" 11 | - "9" 12 | - lts/* 13 | - stable 14 | sudo: false 15 | dist: trusty 16 | addons: 17 | apt: 18 | sources: 19 | - ubuntu-toolchain-r-test 20 | packages: 21 | - gcc-4.8 22 | - g++-4.8 23 | env: 24 | - CC=gcc-4.8 CXX=g++-4.8 25 | before_install: 26 | - | 27 | if [[ ${TRAVIS_OS_NAME} == osx ]]; then 28 | export CXX=clang 29 | export CC=clang 30 | fi 31 | - $CC --version 32 | - $CXX --version 33 | - env 34 | install: 35 | - npm install --build-from-source 36 | before_deploy: 37 | - node_modules/.bin/node-pre-gyp package 38 | deploy: 39 | provider: releases 40 | tag_name: $TRAVIS_TAG 41 | draft: true 42 | api_key: 43 | secure: m+4BLS1/qkn1h//I41aBEAYaTMTFF1uN9KjWqvfDuN8zcQPWIMjkCyBeRtsmgzwtALmRox2WHtrCNjlzxR55i7jS3bI8TD9IqPA34XRtWge2SLuKcuZ/kvKJYhZnoKvX9fLWEZUICwdybGBK6q3CGpEQvBF0Qdvo5T3Xy2qGi30g+HtHJNvedbSkC05/zB8JkLkWEzmBpHZThudrvY8lVyeZ33sszi4QnrKiSyssJPx2cp7sxmUzYTkKO84I+rp/GiamREyD7Bz3rA8//sNlPpdkRr5j65f3WIFAYq6pxDjTFWz8GV4PtH2r7EW5r1NqmUDNILMqGM4Vq2ycG7OtQpewu8uR4c7KT61wQLm/S1hlnf5/w1nsHcaXVstrtn2FgBG9IrkDH5PyCwKIURL+dNE/BqJ2lnqFWCSWPaBG2KhGp+FZewmFBeBTcHrqiMV9tNyLcUtcSAPDpBDpYrJyESTGyZ1P5cUwX6bfb2QT7apyTtVoF6TEuZeV1uTv5UpMd/PRdru9LOuUjsOBxAaxpXDtY0vHQcvWWAR2ymTeDkFjdVJo6DTxOsJixfUGC3+1bd/crZuCFkYdm0tB32AnPlYqLQsZ7RBnjFJqMbimFLZdI2z+c30tdin1Tq+BafPmujEkqYpKWToRusJVYtupLGgarwiK2+qKAIInxA5+aS0= 44 | file: build/stage/gagern/node-libtidy/releases/download/**/*.tar.gz 45 | file_glob: true 46 | skip_cleanup: true 47 | on: 48 | repo: gagern/node-libtidy 49 | tags: true 50 | condition: > 51 | ${TRAVIS_TAG} == v*([0-9]).*([0-9]).*([0-9]) 52 | && ${TRAVIS_NODE_VERSION} == [0-9]* 53 | -------------------------------------------------------------------------------- /util/gen-typescript-decl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const libtidy = require("../"); 6 | 7 | function alphabeticalByName(a, b) { 8 | return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; 9 | } 10 | 11 | function lines() { 12 | const doc = new libtidy.TidyDoc(); 13 | const lines = []; 14 | for (let opt of doc.getOptionList().sort(alphabeticalByName)) { 15 | const name = opt.name.replace(/-/g, "_"); 16 | let values = opt.pickList.map(JSON.stringify); 17 | if (opt.type === "integer") { 18 | if (values.length === 0) 19 | values = ["number"]; 20 | } else if (opt.type === "string") { 21 | if (values.length === 0) 22 | values = ["string"]; 23 | } else if (opt.type === "boolean") { 24 | values = ["boolean"]; 25 | } else { 26 | throw Exception(`Unknown type: ${opt.type}`); 27 | } 28 | lines.push(`${name}?: ${values.join(" | ")}`); 29 | } 30 | return lines; 31 | } 32 | 33 | function generate() { 34 | return `/** 35 | * Type for libtidy options 36 | * @generated with /util/gen-typescript-decl.ts 37 | */ 38 | export namespace Generated { 39 | /** 40 | * NOTE: 41 | * This definition describes the dictionary as returned by the getters. 42 | * When setting value, a lot more is technically possible: 43 | * - setting enum values by number 44 | * - setting boolean values using strings like "yes" 45 | * - using CamelCase instead of _ for multi-word option names 46 | * But none of that is advisable practice in a type-checked setup. 47 | */ 48 | interface OptionDict { 49 | ${lines().join("\n ")} 50 | } 51 | } 52 | `; 53 | } 54 | 55 | function main() { 56 | const dts = path.join(__dirname, "..", "src", "options.d.ts"); 57 | const content = generate(); 58 | 59 | fs.writeFileSync(dts, content); 60 | console.log(`successfully generated ${dts}`); 61 | process.exit(0); 62 | } 63 | 64 | main(); 65 | -------------------------------------------------------------------------------- /test/ts-decl-test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is mostly for the TypeScript declaration and not the behavior 3 | * If this file does not type check, mocha will refuse to start test 4 | */ 5 | 6 | import { expect } from 'chai'; 7 | /// <reference types="node" /> 8 | 9 | import * as libtidy from '../'; 10 | 11 | describe('index.d.ts', () => { 12 | const testDoc1 = Buffer.from('<!DOCTYPE html>\n<html><head></head>\n' + 13 | '<body><p>foo</p></body></html>'); 14 | 15 | let doc: libtidy.TidyDoc; 16 | 17 | beforeEach(() => { 18 | doc = libtidy.TidyDoc(); 19 | }); 20 | 21 | it("has sync tidy API", () => { 22 | // not actually calling the API 23 | if (1) return; 24 | 25 | doc.parseBufferSync(testDoc1); 26 | var res = doc.cleanAndRepairSync(); 27 | const diag: string = doc.runDiagnosticsSync(); 28 | const buf: Buffer = doc.saveBufferSync(); 29 | }); 30 | 31 | it("has async tidy API", () => { 32 | if (1) return; 33 | 34 | doc.parseBuffer(testDoc1, dummyCB); 35 | doc.cleanAndRepair(dummyCB); 36 | doc.runDiagnostics(dummyCB); 37 | doc.saveBuffer(dummyCB); 38 | doc.tidyBuffer(testDoc1, dummyCB); 39 | }); 40 | 41 | it("has option set / get API", () => { 42 | doc.optSet("alt-text", "foo"); 43 | expect(doc.optGet("alt_text")).to.eq('foo'); 44 | expect(doc.optGet("AltText")).to.eq('foo'); 45 | 46 | doc.optSet("wrap", 82); 47 | expect(doc.optGet("Wrap")).to.eq(82); 48 | 49 | doc.options = { 50 | wrap: 83, 51 | input_encoding: "win1252" 52 | }; 53 | 54 | expect(doc.optGet("Wrap")).to.eq(83); 55 | expect(doc.optGet("input-encoding")).to.eq("win1252"); 56 | 57 | expect(doc.getOption('char-encoding')) 58 | .to.be.instanceof(libtidy.TidyOption); 59 | 60 | // libtidy.TidyOption is not callable with () or new 61 | // libtidy.TidyOption(); 62 | // new libtidy.TidyOption); 63 | }); 64 | }); 65 | 66 | function dummyCB(err: Error, result: { errlog: string, output: Buffer }) { } 67 | 68 | // vim: shiftwidth=2 69 | -------------------------------------------------------------------------------- /src/TidyDoc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var lib = require("./lib"); 4 | var TidyDoc = lib.TidyDoc; 5 | module.exports = TidyDoc; 6 | 7 | // Augment native code by some JavaScript-written convenience methods 8 | 9 | TidyDoc.prototype._async1 = function(buf, b1, b2, b3, cb) { 10 | if (cb) 11 | this._async2(buf, b1, b2, b3, res => cb(null, res), err => cb(err)); 12 | else 13 | return new Promise( 14 | (resolve, reject) => this._async2(buf, b1, b2, b3, resolve, reject)); 15 | } 16 | 17 | TidyDoc.prototype.parseBuffer = function(buf, cb) { 18 | return this._async1(buf, false, false, false, cb); 19 | }; 20 | 21 | TidyDoc.prototype.cleanAndRepair = function(cb) { 22 | return this._async1(null, true, false, false, cb); 23 | }; 24 | 25 | TidyDoc.prototype.runDiagnostics = function(cb) { 26 | return this._async1(null, false, true, false, cb); 27 | }; 28 | 29 | TidyDoc.prototype.saveBuffer = function(cb) { 30 | return this._async1(null, false, false, true, cb); 31 | }; 32 | 33 | TidyDoc.prototype.tidyBuffer = function(buf, cb) { 34 | return this._async1(buf, true, true, true, cb); 35 | }; 36 | 37 | Object.defineProperties(TidyDoc.prototype, { 38 | 39 | options: { 40 | configurable: true, 41 | enumerable: true, 42 | get: function() { 43 | var doc = this; 44 | var props = {}; 45 | this.getOptionList().forEach(function(opt) { 46 | var prop = { 47 | configurable: true, 48 | enumerable: true, 49 | get: function() { 50 | return doc.optGet(opt); 51 | }, 52 | set: function(val) { 53 | return doc.optSet(opt, val); 54 | } 55 | }; 56 | var name = opt.toString(); 57 | //props[name] = prop; 58 | props[name.replace(/-/g, '_')] = prop; 59 | }); 60 | var obj = {}; 61 | Object.defineProperties(obj, props); 62 | return obj; 63 | }, 64 | set: function(opts) { 65 | for (var key in opts) 66 | this.optSet(key, opts[key]); 67 | }, 68 | }, 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /src/worker.cc: -------------------------------------------------------------------------------- 1 | #include "node-libtidy.hh" 2 | 3 | namespace node_libtidy { 4 | 5 | TidyWorker::TidyWorker(Doc* doc, 6 | v8::Local<v8::Function> resolve, 7 | v8::Local<v8::Function> reject) 8 | : Nan::AsyncWorker(NULL), doc(doc), resolve(resolve), reject(reject) 9 | { 10 | doc->Lock(); 11 | tidyBufInitWithAllocator(&input, &allocator); 12 | } 13 | 14 | void TidyWorker::setInput(const char* data, size_t length) { 15 | tidyBufAttach(&input, c2b(const_cast<char*>(data)), length); 16 | } 17 | 18 | void TidyWorker::Execute() { 19 | WorkerSentinel sentinel(parent); 20 | rc = 0; 21 | if (rc >= 0 && input.bp) { 22 | lastFunction = "tidyParseString"; 23 | rc = tidyParseBuffer(doc->doc, &input); 24 | } 25 | if (rc >= 0 && shouldCleanAndRepair) { 26 | lastFunction = "tidyCleanAndRepair"; 27 | rc = tidyCleanAndRepair(doc->doc); 28 | } 29 | if (rc >= 0 && shouldRunDiagnostics) { 30 | lastFunction = "tidyRunDiagnostics"; 31 | rc = tidyRunDiagnostics(doc->doc); 32 | } 33 | if (rc >= 0 && shouldSaveToBuffer) { 34 | lastFunction = "tidySaveBuffer"; 35 | rc = tidySaveBuffer(doc->doc, output); 36 | } 37 | } 38 | 39 | void TidyWorker::WorkComplete() { 40 | doc->Unlock(); 41 | v8::Local<v8::Value> args[1]; 42 | Nan::HandleScope scope; 43 | { 44 | Nan::TryCatch tryCatch; 45 | doc->CheckResult(rc, lastFunction); 46 | if (tryCatch.HasCaught()) { 47 | if (!tryCatch.CanContinue()) return; 48 | args[0] = tryCatch.Exception(); 49 | reject(1, args); 50 | return; 51 | } 52 | } 53 | v8::Local<v8::Object> res = Nan::New<v8::Object>(); 54 | if (shouldSaveToBuffer) { 55 | v8::Local<v8::Value> out = Nan::Null(); 56 | if (!output.isEmpty()) 57 | out = output.buffer().ToLocalChecked(); 58 | Nan::Set(res, Nan::New("output").ToLocalChecked(), out); 59 | } 60 | v8::Local<v8::Value> err = doc->err.string().ToLocalChecked(); 61 | Nan::Set(res, Nan::New("errlog").ToLocalChecked(), err); 62 | args[0] = res; 63 | resolve(1, args); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libtidy", 3 | "version": "0.3.8", 4 | "description": "Node bindings to the HTML Tidy library", 5 | "main": "src/index.js", 6 | "bin": { 7 | "htmltidy": "src/cli.js" 8 | }, 9 | "binary": { 10 | "module_name": "tidy", 11 | "module_path": "./lib/", 12 | "host": "https://github.com", 13 | "remote_path": "./gagern/node-libtidy/releases/download/v{version}/", 14 | "package_name": "{node_abi}-{platform}-{arch}.tar.gz" 15 | }, 16 | "types": "src/index.d.ts", 17 | "scripts": { 18 | "install": "node-pre-gyp install --fallback-to-build", 19 | "prepublish": "node util/gen-typescript-decl.js", 20 | "pretest": "node util/gen-typescript-decl.js", 21 | "test": "mocha --compilers ts:ts-node/register" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/gagern/node-libtidy.git" 26 | }, 27 | "keywords": [ 28 | "htmltidy", 29 | "libtidy", 30 | "tidylib", 31 | "html", 32 | "tidy", 33 | "html5", 34 | "beautify", 35 | "format", 36 | "indent" 37 | ], 38 | "author": "Martin von Gagern <Martin.vGagern@gmx.net>", 39 | "contributors": [ 40 | "Raphael Ackermann <raphael.ackermann@gmail.com>", 41 | "Wang Guan <momocraft@gmail.com>" 42 | ], 43 | "license": "MIT", 44 | "gypfile": true, 45 | "bugs": { 46 | "url": "https://github.com/gagern/node-libtidy/issues" 47 | }, 48 | "homepage": "https://github.com/gagern/node-libtidy#readme", 49 | "devDependencies": { 50 | "@types/chai": "^3.4.1", 51 | "@types/mocha": "^2.2.40", 52 | "@types/node": "^7.0.12", 53 | "chai": "^3.4.1", 54 | "chai-subset": "^1.2.0", 55 | "htmltidy2": "^0.1.4", 56 | "mocha": "^3.1.2", 57 | "ts-node": "^3.0.2", 58 | "typescript": "^2.2.2" 59 | }, 60 | "dependencies": { 61 | "nan": "^2.1.0", 62 | "node-pre-gyp": "^0.6.36" 63 | }, 64 | "bundledDependencies": [ 65 | "node-pre-gyp" 66 | ], 67 | "files": [ 68 | "src/", 69 | "test/", 70 | "/*.js", 71 | "/*.md", 72 | "binding.gyp", 73 | "tidy-html5/version.txt", 74 | "tidy-html5/README.md", 75 | "tidy-html5/README/", 76 | "tidy-html5/src/", 77 | "tidy-html5/include/" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /src/memory.cc: -------------------------------------------------------------------------------- 1 | #include "node-libtidy.hh" 2 | 3 | #include <cstdlib> 4 | #include <cstdio> 5 | 6 | namespace node_libtidy { 7 | 8 | void adjustMem(ssize_t diff); 9 | 10 | namespace { 11 | 12 | struct memHdr { 13 | size_t size; 14 | double data; 15 | }; 16 | 17 | inline size_t hdrSize() { 18 | return offsetof(memHdr, data); 19 | } 20 | 21 | inline void* hdr2client(memHdr* hdr) { 22 | return static_cast<void*>(reinterpret_cast<char*>(hdr) + hdrSize()); 23 | } 24 | 25 | inline memHdr* client2hdr(void* client) { 26 | return reinterpret_cast<memHdr*>(static_cast<char*>(client) - hdrSize()); 27 | } 28 | 29 | void* TIDY_CALL myAlloc(TidyAllocator*, size_t size) { 30 | size_t totalSize = size + hdrSize(); 31 | memHdr* mem = static_cast<memHdr*>(std::malloc(totalSize)); 32 | if (!mem) return NULL; 33 | mem->size = size; 34 | adjustMem(totalSize); 35 | return hdr2client(mem); 36 | } 37 | 38 | void* TIDY_CALL myRealloc(TidyAllocator*, void* buf, size_t size) { 39 | memHdr* mem1 = buf ? client2hdr(buf) : NULL; 40 | ssize_t oldSize = mem1 ? ssize_t(mem1->size) : -ssize_t(hdrSize()); 41 | memHdr* mem2 = static_cast<memHdr*>(std::realloc(mem1, size + hdrSize())); 42 | if (!mem2) return NULL; 43 | mem2->size = size; 44 | adjustMem(ssize_t(size) - oldSize); 45 | return hdr2client(mem2); 46 | } 47 | 48 | void TIDY_CALL myFree(TidyAllocator*, void* buf) { 49 | if (!buf) return; 50 | memHdr* mem = client2hdr(buf); 51 | ssize_t totalSize = mem->size + hdrSize(); 52 | adjustMem(-totalSize); 53 | std::free(mem); 54 | } 55 | 56 | void TIDY_CALL myPanic(TidyAllocator*, ctmbstr msg) { 57 | std::fputs(msg, stderr); 58 | std::abort(); 59 | } 60 | 61 | const TidyAllocatorVtbl vtbl = { 62 | myAlloc, 63 | myRealloc, 64 | myFree, 65 | myPanic 66 | }; 67 | 68 | Nan::nauv_key_t tlsKey; 69 | 70 | } 71 | 72 | TidyAllocator allocator = { 73 | &vtbl 74 | }; 75 | 76 | void adjustMem(ssize_t diff) { 77 | WorkerSentinel* worker = 78 | static_cast<WorkerSentinel*>(Nan::nauv_key_get(&tlsKey)); 79 | if (worker) { 80 | worker->parent.memAdjustments += diff; 81 | return; 82 | } 83 | 84 | // if v8 is no longer running, don't try to adjust memory 85 | // this happens when the v8 vm is shutdown and the program is exiting 86 | // our cleanup routines for libxml will be called (freeing memory) 87 | // but v8 is already offline and does not need to be informed 88 | // trying to adjust after shutdown will result in a fatal error 89 | #if (NODE_MODULE_VERSION > 0x000B) 90 | if (v8::Isolate::GetCurrent() == 0) { 91 | assert(diff <= 0); 92 | return; 93 | } 94 | #endif 95 | #if (NODE_MODULE_VERSION > 46) 96 | if (v8::Isolate::GetCurrent()->IsDead()) 97 | #else 98 | if (v8::V8::IsDead()) 99 | #endif 100 | { 101 | assert(diff <= 0); 102 | return; 103 | } 104 | Nan::AdjustExternalMemory(diff); 105 | } 106 | 107 | void initMemory() { 108 | Nan::nauv_key_create(&tlsKey); 109 | } 110 | 111 | // Set up in V8 thread 112 | WorkerParent::WorkerParent() : memAdjustments(0) { 113 | } 114 | 115 | // Tear down in V8 thread 116 | WorkerParent::~WorkerParent() { 117 | Nan::AdjustExternalMemory(memAdjustments); 118 | } 119 | 120 | // Set up in worker thread 121 | WorkerSentinel::WorkerSentinel(WorkerParent& parent) : parent(parent) { 122 | Nan::nauv_key_set(&tlsKey, this); 123 | } 124 | 125 | // Tear down in worker thread 126 | WorkerSentinel::~WorkerSentinel() { 127 | Nan::nauv_key_set(&tlsKey, NULL); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for node-libtidy v0.3 2 | // Project: node-libtidy / https://github.com/gagern/node-libtidy 3 | // Definitions by: Wang Guan <momocraft@gmail.com> 4 | // vim: shiftwidth=2 5 | 6 | export const tidyBuffer: TidyBufferStatic 7 | export const TidyDoc: TidyDocConstructor 8 | export const compat: TidyCompat 9 | 10 | /// <reference types="node" /> 11 | import { Generated } from './options'; 12 | 13 | /** 14 | * Callback convention: the result 15 | */ 16 | interface TidyResult { 17 | /** 18 | * errlog contains the error messages generated during the run, 19 | * formatted as a string including a trailing newline. 20 | */ 21 | errlog?: string 22 | /** 23 | * output contains the output buffer if output was generated. 24 | * The property is unset if generating output was not part of the method 25 | * in question, or null if no output was generated due to errors. 26 | */ 27 | output?: Buffer 28 | } 29 | 30 | /** 31 | * Callback convention: the signerature used in async APIs 32 | */ 33 | interface TidyCallback { 34 | (err: Error | null, res: TidyResult | null): void 35 | } 36 | 37 | /** 38 | * High-level functions automate the most common workflows. 39 | * 40 | * The document is assumed to be a buffer or a string. 41 | * Anything else will be converted to a string and then 42 | * turned into a buffer. 43 | */ 44 | interface TidyBufferStatic { 45 | (document: string | Buffer, options: Generated.OptionDict, 46 | callback: TidyCallback): void 47 | 48 | (document: string | Buffer, callback: TidyCallback): void 49 | } 50 | 51 | export class TidyOption { 52 | // creation of TidyOption is not exposed 53 | private constructor() 54 | readonly name: string 55 | readonly type: "boolean" | "integer" | "doctype" | "string" 56 | readonly readOnly: boolean 57 | readonly pickList: string[] 58 | readonly id: number 59 | readonly category: string 60 | toString(): string 61 | } 62 | 63 | type TidyOptionKey = TidyOption | string | number // object or name or id 64 | type TidyOptionValue = boolean | number | string | null 65 | 66 | /** 67 | * TidyDoc is the central object for dealing with the library at a low level 68 | * 69 | * Such an object will hold a configuration and be able to process one input 70 | * file at a time, while multiple such objects can deal with multiple inputs 71 | * simultaneously using an independent configuration for each of them. 72 | */ 73 | interface TidyDoc { 74 | // Sync calls 75 | cleanAndRepairSync(): string 76 | parseBufferSync(document: Buffer): string 77 | runDiagnosticsSync(): string 78 | saveBufferSync(): Buffer 79 | // getErrorLog(): string // is not needed: other calls already return log 80 | 81 | // Async calls 82 | cleanAndRepair(callback: TidyCallback): void 83 | parseBuffer(document: Buffer, callback: TidyCallback): void 84 | runDiagnostics(callback: TidyCallback): void 85 | saveBuffer(callback: TidyCallback): void 86 | tidyBuffer(buf: Buffer, callback: TidyCallback): void 87 | 88 | // batch set/get of options 89 | options: Generated.OptionDict 90 | // Methods that return TidyOption object 91 | getOptionList(): TidyOption[] 92 | getOption(key: TidyOptionKey): TidyOption 93 | 94 | // Working with individual options 95 | optGet(key: TidyOptionKey): TidyOptionValue 96 | optSet(key: TidyOptionKey, val: TidyOptionValue): void 97 | optGetDoc(key: TidyOptionKey): string 98 | optGetDocLinksList(key: TidyOptionKey): TidyOption[] 99 | optGetCurrPick(key: TidyOptionKey): string | null 100 | } 101 | 102 | /** 103 | * Constructor 104 | * (Can be used with `new` or normal call) 105 | */ 106 | interface TidyDocConstructor { 107 | new (): TidyDoc 108 | (): TidyDoc 109 | } 110 | 111 | /** 112 | * Elements of the compat namespace offer compatibility with other 113 | * libtidy bindings for node. 114 | */ 115 | interface TidyCompat { 116 | libtidy: { 117 | tidy: { 118 | (text: string, callback: (err: any, html: string) => void): void 119 | (text: string, options: any, callback: (err: any, html: string) => void): void 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': '<(module_name)', 5 | 'sources': [ 6 | 'src/node-libtidy.cc', 7 | 'src/memory.cc', 8 | 'src/opt.cc', 9 | 'src/doc.cc', 10 | 'src/worker.cc', 11 | 'tidy-html5/src/access.c', 12 | 'tidy-html5/src/attrs.c', 13 | 'tidy-html5/src/istack.c', 14 | 'tidy-html5/src/parser.c', 15 | 'tidy-html5/src/tags.c', 16 | 'tidy-html5/src/entities.c', 17 | 'tidy-html5/src/lexer.c', 18 | 'tidy-html5/src/pprint.c', 19 | 'tidy-html5/src/charsets.c', 20 | 'tidy-html5/src/clean.c', 21 | 'tidy-html5/src/message.c', 22 | 'tidy-html5/src/config.c', 23 | 'tidy-html5/src/alloc.c', 24 | 'tidy-html5/src/attrask.c', 25 | 'tidy-html5/src/attrdict.c', 26 | 'tidy-html5/src/attrget.c', 27 | 'tidy-html5/src/buffio.c', 28 | 'tidy-html5/src/fileio.c', 29 | 'tidy-html5/src/streamio.c', 30 | 'tidy-html5/src/tagask.c', 31 | 'tidy-html5/src/tmbstr.c', 32 | 'tidy-html5/src/utf8.c', 33 | 'tidy-html5/src/tidylib.c', 34 | 'tidy-html5/src/mappedio.c', 35 | 'tidy-html5/src/gdoc.c', 36 | 'tidy-html5/src/language.c', 37 | ], 38 | 'include_dirs': [ 39 | 'tidy-html5/include', 40 | '<!(node -e "require(\'nan\')")' 41 | ], 42 | 'defines': [ 43 | '_REENTRANT', 44 | 'HAVE_CONFIG_H', 45 | 'SUPPORT_UTF16_ENCODINGS=1', 46 | 'SUPPORT_ASIAN_ENCODINGS=1', 47 | 'SUPPORT_ACCESSIBILITY_CHECKS=1', 48 | 'LIBTIDY_VERSION="<!(node parse-version.js version)"', 49 | 'RELEASE_DATE="<!(node parse-version.js date)"', 50 | 'BUILD_SHARED_LIB', 51 | 'BUILDING_SHARED_LIB', 52 | ], 53 | 'configurations': { 54 | 'Debug': { 55 | 'defines': [ 56 | 'DEBUG', 57 | ], 58 | 'msvs_settings': { 59 | 'VCCLCompilerTool': { 60 | 'RuntimeLibrary': 1, # rtMultiThreadedDebug 61 | }, 62 | }, 63 | }, 64 | 'Release': { 65 | 'defines': [ 66 | 'NDEBUG', 67 | ], 68 | 'msvs_settings': { 69 | 'VCCLCompilerTool': { 70 | 'RuntimeLibrary': 0, # rtMultiThreaded 71 | }, 72 | }, 73 | }, 74 | }, 75 | 'conditions': [ 76 | ['OS=="win"', { 77 | 'sources': [ 78 | 'tidy-html5/src/sprtf.c', 79 | ], 80 | 'defines': [ 81 | 'NOMINMAX', 82 | '_USE_MATH_DEFINES', 83 | '_CRT_SECURE_NO_WARNINGS', 84 | '_SCL_SECURE_NO_WARNINGS', 85 | '__CRT_NONSTDC_NO_WARNINGS', 86 | ] 87 | }, { 88 | 'cflags': [ 89 | '-Wno-missing-field-initializers', 90 | ], 91 | }] 92 | ] 93 | }, 94 | { 95 | 'target_name': 'action_after_build', 96 | 'type': 'none', 97 | 'dependencies': [ '<(module_name)' ], 98 | 'copies': [ 99 | { 100 | 'files': [ '<(PRODUCT_DIR)/<(module_name).node' ], 101 | 'destination': '<(module_path)' 102 | } 103 | ] 104 | }, 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | const libtidy = require("../"); 6 | 7 | function OptionHandler(doc, args) { 8 | this.doc = doc; 9 | doc.options = { 10 | newline: "LF", 11 | }; 12 | doc.getOption("char-encoding").pickList.forEach( 13 | enc => this["-" + enc] = () => doc.options.char_encoding = enc); 14 | const positional = []; 15 | for (let i = 0; i < args.length; ++i) { 16 | const arg = args[i]; 17 | if (!arg.startsWith("-")) { 18 | positional.push(arg); 19 | continue; 20 | } else if (arg === "--") { 21 | return positional.concat(args.slice(i + 1)); 22 | } 23 | let h = this[arg]; 24 | while (typeof h === "string" && h[0] === "-") 25 | h = this[h]; 26 | if (typeof h === "string") { 27 | if (h.endsWith("=")) { 28 | if (++i === args.length) { 29 | console.error(`Missing argument for ${arg}`); 30 | process.exit(1); 31 | } 32 | doc.optSet(h.substr(0, h.length - 1), args[i]); 33 | } else { 34 | doc.optSet(h, "yes"); 35 | } 36 | } else if (typeof h === "function") { 37 | if (i + h.length >= args.length) { 38 | console.error(`Missing argument for ${arg}`); 39 | process.exit(1); 40 | } 41 | h.apply(this, args.slice(i + 1, i + h.length + 1)); 42 | i += h.length; 43 | } else if (typeof h === "undefined" && arg.startsWith("--")) { 44 | if (++i === args.length) { 45 | console.error(`Missing argument for ${arg}`); 46 | process.exit(1); 47 | } 48 | doc.optSet(arg.substr(2), args[i]); 49 | } else { 50 | console.error(`Unknown argument: ${arg}`); 51 | process.exit(1); 52 | } // end of case distinction for typeof h 53 | } // end of for loop 54 | return positional; 55 | }; 56 | 57 | OptionHandler.prototype = { 58 | "-xml": "input-xml", 59 | "-asxml": "output-xhtml", 60 | "-asxhtml": "output-xhtml", 61 | "-ashtml": "output-html", 62 | "-i": "-indent", 63 | "-indent": function() { 64 | this.doc.options.indent = "auto"; 65 | if (this.doc.options.indent_spaces == 0) 66 | this.doc.optResetToDefault("indent-spaces"); 67 | }, 68 | "-omit": "omit-optional-tags", 69 | "-u": "-upper", 70 | "-upper": "uppercase-tags", 71 | "-c": "-clean", 72 | "-clean": "clean", 73 | "-g": "-gdoc", 74 | "-gdoc": "gdoc", 75 | "-b": "-bare", 76 | "-bare": "bare", 77 | "-n": "-numeric", 78 | "-numeric": "numeric-entities", 79 | "-m": "-modify", 80 | "-modify": "write-back", 81 | "-e": "-errors", 82 | "-errors": "markup", 83 | "-q": "-quiet", 84 | "-quiet": "quiet", 85 | "-language": "-lang", 86 | "-lang": "language=", 87 | "--help": "-help", 88 | "-h": "-help", 89 | "-help": function() { 90 | console.log("No usage help available at this point."); 91 | console.log("Most options follow the tidy-html5 command line too, though."); 92 | console.log("So eithe read the sources or its documentation."); 93 | process.exit(0); 94 | }, 95 | "--output-file": "-output", 96 | "-o": "-output", 97 | "-output": "output-file", 98 | "--file": "-file", 99 | "-f": "-file", 100 | "-file": "error-file", 101 | "--wrap": "-wrap", 102 | "-w": "-wrap", 103 | "-wrap": "wrap=", 104 | "--version": "-version", 105 | "-v": "-version", 106 | "-version": function() { 107 | var package_info = require("../package.json") 108 | console.log( 109 | `Node module ${package_info.name} version ${package_info.version}`); 110 | console.log(`Native library libtidy version ${libtidy.libraryVersion}`); 111 | process.exit(0); 112 | }, 113 | "-access": "accessibility-check=", 114 | }; 115 | 116 | function main(args) { 117 | const doc = new libtidy.TidyDoc(); 118 | const positional = new OptionHandler(doc, args); 119 | const opts = {}; 120 | for (let opt of doc.getOptionList()) { 121 | let value = doc.optGet(opt); 122 | if (!opt.readOnly && value != opt.default) 123 | opts[opt] = value; 124 | } 125 | let promise = null; 126 | if (doc.options.write_back) { 127 | promise = libtidy.tidyFilesInPlace(positional, opts); 128 | } else if (positional.length > 1) { 129 | console.error("Cannot tidy more than one file unless -modify is specified"); 130 | process.exit(1); 131 | } else { 132 | if (positional.length === 0) 133 | promise = libtidy.readStdin(); 134 | else 135 | promise = libtidy.readFile(positional[0]); 136 | promise = promise.then(libtidy.tidyUp(opts)); 137 | if (doc.options.output_file) 138 | promise = promise.then(libtidy.writeFile(doc.options.output_file)); 139 | else 140 | promise = promise.then(libtidy.writeStdout()); 141 | } 142 | promise.then( 143 | res => process.exit(0), 144 | err => { console.error(String(err)); process.exit(2); }); 145 | } 146 | 147 | if (require.main === module) 148 | main(process.argv.slice(2)) 149 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | 5 | var lib = require("./lib"); 6 | for (let key in lib) 7 | module.exports[key] = lib[key]; 8 | 9 | module.exports.nodeModuleVersion = require("../package.json").version; 10 | 11 | var TidyDoc = require("./TidyDoc"); 12 | 13 | module.exports.compat = require("./compat"); 14 | 15 | class TidyException extends Error { 16 | constructor(message, opts) { 17 | this.message = message; 18 | for (var key in opts) 19 | this[key] = opts[key]; 20 | Error.captureStackTrace(this, TidyException); 21 | } 22 | } 23 | 24 | function tidyBuffer(buf, opts, cb) { 25 | if (typeof cb === "undefined" && typeof opts === "function") { 26 | cb = opts; 27 | opts = {}; 28 | } 29 | opts = opts || {}; 30 | var doc = TidyDoc(); 31 | doc.options = { 32 | newline: "LF", 33 | }; 34 | doc.options = opts; 35 | if (!Buffer.isBuffer(buf)) 36 | buf = Buffer(String(buf)); 37 | return doc.tidyBuffer(buf, cb); // can handle both cb and promise 38 | } 39 | 40 | function readFile(name) { 41 | return new Promise((resolve, reject) => 42 | fs.readFile(name, (err, content) => { 43 | if (err) reject(err); 44 | else resolve({name: name, buf: content}); 45 | })); 46 | } 47 | 48 | function readStream(stream, name) { 49 | name = name || "<stream>"; 50 | return new Promise((resolve, reject) => { 51 | const chunks = []; 52 | stream.once("error", reject); 53 | stream.on("data", chunk => chunks.append(chunk)); 54 | stream.on("end", resolve({name: name, buf: Buffer.concat(chunks)})); 55 | }); 56 | } 57 | 58 | function readStdin() { 59 | return readStream(process.stdin, "<stdin>"); 60 | } 61 | 62 | function readBuffer(stream, buf) { 63 | name = name || "<buffer>"; 64 | return new Promise((resolve, reject) => resolve({name: name, buf: buf})); 65 | } 66 | 67 | function tidyUp(opts) { 68 | return input => 69 | tidyBuffer(input.buf, opts).then(res => { 70 | if (!res.output) 71 | throw new TidyException(`Failed to parse ${input.name}`, res); 72 | res.inputName = input.name; 73 | return res; 74 | }); 75 | } 76 | 77 | function writeFile(name) { 78 | return res => 79 | new Promise((resolve, reject) => 80 | fs.writeFile(name, res.output, err => { 81 | if (err) return reject(err); 82 | delete res.output; // save memory 83 | res.outputName = name; 84 | resolve(res); 85 | })); 86 | } 87 | 88 | function writeStreamUnchecked(stream, name) { 89 | name = name || "<stream>"; 90 | return res => { 91 | stream.write(res.output); 92 | delete res.output; // save memory 93 | res.outputName = name; 94 | return res; 95 | }; 96 | } 97 | 98 | function writeStreamChecked(stream, name) { 99 | name = name || "<stream>"; 100 | return res => 101 | new Promise((resolve, reject) => { 102 | stream.once("error", reject); 103 | stream.once("finish", resolve); 104 | stream.end(res.output); 105 | }).then(() => { 106 | delete res.output; // save memory 107 | res.outputName = name; 108 | return res; 109 | }); 110 | } 111 | 112 | function writeStdout() { 113 | return writeStreamUnchecked(process.stdout, "<stdout>"); 114 | } 115 | 116 | function promiseOrCallback(cb, f) { 117 | if (cb) f().then(res => cb(null, res), err => cb(err)); 118 | else return f(); 119 | } 120 | 121 | function tidyFileToFile(input, output, opts, cb) { 122 | if (typeof opts === "undefined" && 123 | typeof cb === "undefined" && 124 | typeof output === "function") { 125 | cb = output; 126 | output = input; 127 | opts = {}; 128 | } else if (typeof cb === "undefined" && typeof opts === "function") { 129 | cb = opts; 130 | if (typeof output === "string") { 131 | opts = {}; 132 | } else { 133 | opts = output; 134 | output = input; 135 | } 136 | } 137 | return promiseOrCallback(cb, () => 138 | readFile(input).then(tidyUp(opts)).then(writeFile(output))); 139 | }; 140 | 141 | function tidyFilesInPlace(files, opts, cb) { 142 | if (typeof cb === "undefined" && typeof opts === "function") { 143 | cb = opts; 144 | opts = {}; 145 | } 146 | return promiseOrCallback(cb, () => 147 | Promise.all(Array.from(files, file => tidyFileToFile(file, file, opts)))); 148 | } 149 | 150 | module.exports.tidyBuffer = tidyBuffer; 151 | module.exports.readFile = readFile; 152 | module.exports.readStream = readStream; 153 | module.exports.readStdin = readStdin; 154 | module.exports.readBuffer = readBuffer; 155 | module.exports.tidyUp = tidyUp; 156 | module.exports.writeFile = writeFile; 157 | module.exports.writeStreamUnchecked = writeStreamUnchecked; 158 | module.exports.writeStreamChecked = writeStreamChecked; 159 | module.exports.writeStdout = writeStdout; 160 | module.exports.promiseOrCallback = promiseOrCallback; 161 | module.exports.tidyFileToFile = tidyFileToFile; 162 | module.exports.tidyFilesInPlace = tidyFilesInPlace; 163 | -------------------------------------------------------------------------------- /src/opt.cc: -------------------------------------------------------------------------------- 1 | #include "node-libtidy.hh" 2 | 3 | namespace node_libtidy { 4 | 5 | namespace Opt { 6 | 7 | Nan::Persistent<v8::FunctionTemplate> constructorTemplate; 8 | Nan::Persistent<v8::Function> constructor; 9 | 10 | NAN_MODULE_INIT(Init) { 11 | v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New); 12 | tpl->SetClassName(Nan::New("TidyOption").ToLocalChecked()); 13 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 14 | v8::Local<v8::ObjectTemplate> itpl = tpl->InstanceTemplate(); 15 | 16 | Nan::SetPrototypeMethod(tpl, "toString", toString); 17 | SetAccessor(tpl, itpl, "category", getCategory); 18 | SetAccessor(tpl, itpl, "default", getDefault); 19 | SetAccessor(tpl, itpl, "id", getId); 20 | SetAccessor(tpl, itpl, "name", getName); 21 | SetAccessor(tpl, itpl, "pickList", getPickList); 22 | SetAccessor(tpl, itpl, "readOnly", getReadOnly); 23 | SetAccessor(tpl, itpl, "type", getType); 24 | 25 | constructorTemplate.Reset(tpl); 26 | constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked()); 27 | Nan::Set(target, Nan::New("TidyOption").ToLocalChecked(), 28 | Nan::GetFunction(tpl).ToLocalChecked()); 29 | } 30 | 31 | NAN_METHOD(New) { 32 | if (info.IsConstructCall()) { 33 | Nan::SetInternalFieldPointer(info.This(), 0, NULL); 34 | info.GetReturnValue().Set(info.This()); 35 | } else { 36 | v8::Local<v8::Function> cons = Nan::New(constructor); 37 | info.GetReturnValue().Set(Nan::NewInstance(cons).ToLocalChecked()); 38 | } 39 | } 40 | 41 | v8::Local<v8::Object> Create(TidyOption opt) { 42 | Nan::EscapableHandleScope scope; 43 | v8::Local<v8::Function> cons = Nan::New(constructor); 44 | v8::Local<v8::Object> obj = Nan::NewInstance(cons).ToLocalChecked(); 45 | Nan::SetInternalFieldPointer 46 | (obj, 0, const_cast<void*>(reinterpret_cast<const void*>(opt))); 47 | return scope.Escape(obj); 48 | } 49 | 50 | TidyOption Unwrap(v8::Local<v8::Value> value) { 51 | v8::Local<v8::FunctionTemplate> tpl = Nan::New(constructorTemplate); 52 | if (!tpl->HasInstance(value)) { 53 | Nan::ThrowTypeError("Not a valid TidyOption object"); 54 | return NULL; 55 | } 56 | v8::Local<v8::Object> object = 57 | Nan::To<v8::Object>(value).ToLocalChecked(); 58 | if (object->InternalFieldCount() < 1) { 59 | Nan::ThrowTypeError("Not a valid TidyOption object"); 60 | return NULL; 61 | } 62 | TidyOption opt = reinterpret_cast<TidyOption> 63 | (Nan::GetInternalFieldPointer(object, 0)); 64 | if (opt == NULL) { 65 | Nan::ThrowTypeError("TidyOption not properly initialized"); 66 | return NULL; 67 | } 68 | return opt; 69 | } 70 | 71 | NAN_METHOD(toString) { 72 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 73 | const char* res = tidyOptGetName(opt); 74 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 75 | } 76 | 77 | NAN_PROPERTY_GETTER(getCategory) { 78 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 79 | const char* res; 80 | switch (tidyOptGetCategory(opt)) { 81 | case TidyMarkup: 82 | res = "Markup"; 83 | break; 84 | case TidyDiagnostics: 85 | res = "Diagnostics"; 86 | break; 87 | case TidyPrettyPrint: 88 | res = "PrettyPrint"; 89 | break; 90 | case TidyEncoding: 91 | res = "Encoding"; 92 | break; 93 | case TidyMiscellaneous: 94 | res = "Miscellaneous"; 95 | break; 96 | default: 97 | Nan::ThrowError("Unknown option category"); 98 | return; 99 | } 100 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 101 | } 102 | 103 | NAN_PROPERTY_GETTER(getDefault) { 104 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 105 | switch (tidyOptGetType(opt)) { 106 | case TidyBoolean: 107 | info.GetReturnValue().Set(Nan::New<v8::Boolean> 108 | (bb(tidyOptGetDefaultBool(opt)))); 109 | break; 110 | case TidyInteger: 111 | info.GetReturnValue().Set(Nan::New<v8::Number> 112 | (tidyOptGetDefaultInt(opt))); 113 | break; 114 | default: 115 | const char* res = tidyOptGetDefault(opt); 116 | if (res) 117 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 118 | else 119 | info.GetReturnValue().Set(Nan::Null()); 120 | break; 121 | } 122 | } 123 | 124 | NAN_PROPERTY_GETTER(getId) { 125 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 126 | double res = tidyOptGetId(opt); 127 | info.GetReturnValue().Set(Nan::New(res)); 128 | } 129 | 130 | NAN_PROPERTY_GETTER(getName) { 131 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 132 | const char* res = tidyOptGetName(opt); 133 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 134 | } 135 | 136 | NAN_PROPERTY_GETTER(getPickList) { 137 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 138 | v8::Local<v8::Array> arr = Nan::New<v8::Array>(); 139 | TidyIterator iter = tidyOptGetPickList(opt); 140 | while (iter) { 141 | const char* pick = tidyOptGetNextPick(opt, &iter); 142 | v8::Local<v8::Value> str = Nan::New<v8::String>(pick).ToLocalChecked(); 143 | Nan::Set(arr, arr->Length(), str); 144 | } 145 | info.GetReturnValue().Set(arr); 146 | } 147 | 148 | NAN_PROPERTY_GETTER(getReadOnly) { 149 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 150 | info.GetReturnValue().Set(Nan::New<v8::Boolean>(bb(tidyOptIsReadOnly(opt)))); 151 | } 152 | 153 | NAN_PROPERTY_GETTER(getType) { 154 | TidyOption opt = Unwrap(info.Holder()); if (!opt) return; 155 | const char* res; 156 | switch (tidyOptGetType(opt)) { 157 | case TidyBoolean: 158 | res = "boolean"; 159 | break; 160 | case TidyInteger: 161 | res = "integer"; 162 | break; 163 | case TidyString: 164 | res = "string"; 165 | break; 166 | default: 167 | Nan::ThrowError("Unknown option type"); 168 | return; 169 | } 170 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 171 | } 172 | 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /test/opt-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | chai.use(require("chai-subset")); 5 | var expect = chai.expect; 6 | var util = require("util"); 7 | var libtidy = require("../"); 8 | var TidyDoc = libtidy.TidyDoc; 9 | var TidyOption = libtidy.TidyOption; 10 | 11 | describe("TidyOption:", function() { 12 | 13 | describe("naming:", function() { 14 | 15 | it("with hyphens", function() { 16 | var doc = TidyDoc(); 17 | doc.optSet("alt-text", "foo"); 18 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 19 | }); 20 | 21 | it("with underscores", function() { 22 | var doc = TidyDoc(); 23 | doc.optSet("alt_text", "foo"); 24 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 25 | }); 26 | 27 | it("with camelCase", function() { 28 | var doc = TidyDoc(); 29 | doc.optSet("altText", "foo"); 30 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 31 | }); 32 | 33 | it("mixed", function() { 34 | var doc = TidyDoc(); 35 | doc.optSet("add_xmlDecl", true); 36 | expect(doc.optGet("add-xml_Decl")).to.be.true; 37 | }); 38 | 39 | }); 40 | 41 | describe("getting and setting values:", function() { 42 | 43 | it("setting should affect the value", function() { 44 | var doc = TidyDoc(); 45 | expect(doc.optGet("alt-text")).to.be.null; 46 | expect(doc.optSet("alt-text", "foo")).to.be.undefined; 47 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 48 | }); 49 | 50 | it("null or undefined clear string settings", function() { 51 | var doc = TidyDoc(); 52 | expect(doc.optSet("alt-text", "foo")).to.be.undefined; 53 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 54 | expect(doc.optSet("alt-text", null)).to.be.undefined; 55 | expect(doc.optGet("alt-text")).to.be.null; 56 | expect(doc.optSet("alt-text", "bar")).to.be.undefined; 57 | expect(doc.optGet("alt-text")).to.be.equal("bar"); 58 | expect(doc.optSet("alt-text")).to.be.undefined; 59 | expect(doc.optGet("alt-text")).to.be.null; 60 | }); 61 | 62 | it("objects get stringified", function() { 63 | var doc = TidyDoc(); 64 | doc.optSet("alt-text", [1, 2]); 65 | expect(doc.optGet("alt-text")).to.be.equal("1,2"); 66 | }); 67 | 68 | it("invalid keys throw", function() { 69 | var doc = TidyDoc(); 70 | expect(function() { 71 | doc.optGet("no_suchOption") 72 | }).to.throw(Error, /'no-such-option'/); 73 | }); 74 | 75 | it("can handle boolean options", function() { 76 | var doc = TidyDoc(); 77 | expect(doc.optGet("add-xml-decl")).to.be.false; 78 | expect(doc.optSet("add-xml-decl", "this starts with T")).to.be.undefined; 79 | expect(doc.optGet("add-xml-decl")).to.be.true; 80 | expect(doc.optSet("add-xml-decl", false)).to.be.undefined; 81 | expect(doc.optGet("add-xml-decl")).to.be.false; 82 | expect(function() { 83 | doc.optSet("add-xml-decl", "some strange value"); 84 | }).to.throw(); 85 | }); 86 | 87 | it("setting a readonly option throws", function() { 88 | var doc = TidyDoc(); 89 | var opt = doc.getOption('doctype-mode'); 90 | // If readOnly changes in future, this will help to locate the fail. 91 | expect(opt.readOnly).to.be.true; 92 | expect(function() { 93 | doc.optSet('doctype-mode'); 94 | }).to.throw(Error, /' is readonly/); 95 | }); 96 | 97 | it("can handle integer options", function() { 98 | var doc = TidyDoc(); 99 | expect(doc.optGet("tab-size")).to.be.equal(8); 100 | expect(doc.optSet("tab-size", 3)).to.be.undefined; 101 | expect(doc.optGet("tab-size")).to.be.equal(3); 102 | expect(doc.optSet("tab-size", "5")).to.be.undefined; 103 | expect(doc.optGet("tab-size")).to.be.equal(5); 104 | expect(doc.optSet("tab-size", 2.2)).to.be.undefined; 105 | expect(doc.optGet("tab-size")).to.be.equal(2); 106 | expect(function() { doc.optSet("tab-size", "not a number"); }).to.throw(); 107 | }); 108 | 109 | it("can handle the char-encoding option", function() { 110 | var doc = TidyDoc(); 111 | var utf8 = doc.optGet("char-encoding"); 112 | expect(doc.optSet("char-encoding", "utf8")).to.be.undefined; 113 | expect(doc.optGet("char-encoding")).to.be.equal(utf8); 114 | expect(doc.optSet("char-encoding", "latin1")).to.be.undefined; 115 | expect(doc.optGet("char-encoding")).to.be.not.equal(utf8); 116 | expect(function() { doc.optSet("char-encoding", "foobar"); }).to.throw(); 117 | }); 118 | 119 | it("can handle the newline option", function() { 120 | var doc = TidyDoc(); 121 | expect(doc.optSet("newline", 0)).to.be.undefined; 122 | expect(doc.optGet("newline")).to.be.equal("LF"); 123 | expect(doc.optSet("newline", "crlf")).to.be.undefined; 124 | expect(doc.optGet("newline")).to.be.equal("CRLF"); 125 | expect(doc.optSet("newline", 2)).to.be.undefined; 126 | expect(doc.optGet("newline")).to.be.equal("CR"); 127 | expect(function() { doc.optSet("newline", "yes"); }).to.throw(); 128 | }); 129 | 130 | it("can handle AutoBool options", function() { 131 | var doc = TidyDoc(); 132 | expect(doc.optGet("indent")).to.be.equal("no"); 133 | expect(doc.optSet("indent", "auto")).to.be.undefined; 134 | expect(doc.optGet("indent")).to.be.equal("auto"); 135 | expect(doc.optSet("indent", "yes")).to.be.undefined; 136 | expect(doc.optGet("indent")).to.be.equal("yes"); 137 | expect(doc.optSet("indent", false)).to.be.undefined; 138 | expect(doc.optGet("indent")).to.be.equal("no"); 139 | expect(doc.optSet("indent", true)).to.be.undefined; 140 | expect(doc.optGet("indent")).to.be.equal("yes"); 141 | expect(function() { doc.optSet("indent", "unknown"); }).to.throw(); 142 | }); 143 | 144 | it("can find current value from enum", function() { 145 | var doc = TidyDoc(); 146 | expect(doc.optGetCurrPick("indent")).to.be.equal("no"); 147 | expect(doc.optSet("indent", "auto")).to.be.undefined; 148 | expect(doc.optGetCurrPick("indent")).to.be.equal("auto"); 149 | expect(doc.optGetCurrPick("alt-text")).to.be.null; 150 | expect(doc.optGetCurrPick("input-xml")).to.be.equal("no"); 151 | }); 152 | 153 | it("only affect one document", function() { 154 | var doc1 = TidyDoc(); 155 | var doc2 = TidyDoc(); 156 | doc1.optSet("alt-text", "foo"); 157 | expect(doc1.optGet("alt-text")).to.be.equal("foo"); 158 | expect(doc2.optGet("alt-text")).to.be.null; 159 | expect(TidyDoc().optGet("alt-text")).to.be.null; 160 | }); 161 | 162 | }); 163 | 164 | describe("Documentation for options:", function() { 165 | 166 | it("optGetDoc", function() { 167 | var doc = TidyDoc(); 168 | expect(doc.optGetDoc("newline")).to.match(/Mac OS/); 169 | }); 170 | 171 | it("optGetDocLinksList", function() { 172 | var doc = TidyDoc(); 173 | var links = doc.optGetDocLinksList("char-encoding"); 174 | expect(links).to.be.instanceof(Array); 175 | expect(links).to.have.length.above(1); 176 | expect(links[0]).to.be.instanceof(TidyOption); 177 | expect(links.map(String)) 178 | .to.containSubset(['input-encoding', 'output-encoding']); 179 | }); 180 | 181 | }); 182 | 183 | describe("dealing with TidyOption objects:", function() { 184 | 185 | it("lookup by name", function() { 186 | var doc = TidyDoc(); 187 | var opt = doc.getOption("tabSize"); 188 | expect(opt).to.be.instanceof(TidyOption); 189 | }); 190 | 191 | it("accessors", function() { 192 | var doc = TidyDoc(); 193 | var opt = doc.getOption("tabSize"); 194 | expect(opt.name).to.be.equal("tab-size"); 195 | expect(opt.id).to.be.a("number"); 196 | expect(opt.default).to.be.equal(8); 197 | expect(opt.readOnly).to.be.false; 198 | expect(opt.type).to.be.equal("integer"); 199 | expect(opt.category).to.be.equal("PrettyPrint"); 200 | expect(doc.getOption("indent").pickList).to.eql(["no", "yes", "auto"]); 201 | }); 202 | 203 | it("lookup by id", function() { 204 | var doc = TidyDoc(); 205 | var opt = doc.getOption("tab_size"); 206 | opt = doc.getOption(+opt.id); 207 | expect(opt.name).to.be.equal("tab-size"); 208 | }); 209 | 210 | it("for getting", function() { 211 | var doc = TidyDoc(); 212 | var opt = doc.getOption("tab-size"); 213 | expect(doc.optGet(opt)).to.be.equal(8); 214 | }); 215 | 216 | it("for setting", function() { 217 | var doc = TidyDoc(); 218 | var opt = doc.getOption("tabSize"); 219 | expect(doc.optSet(opt, 3)).to.be.undefined; 220 | expect(doc.optGet("tab_size")).to.be.equal(3); 221 | }); 222 | 223 | it("listing options", function() { 224 | var doc = TidyDoc(); 225 | var opts = doc.getOptionList(); 226 | expect(opts).to.have.length.above(80); 227 | var tabSize = opts.filter(function(opt) { 228 | return opt.name == "tab-size"; 229 | }) 230 | expect(tabSize).to.have.length(1); 231 | tabSize = tabSize[0]; 232 | expect(tabSize).to.be.an.instanceof(TidyOption); 233 | expect(doc.optGet(tabSize)).to.be.equal(8); 234 | }); 235 | 236 | }); 237 | 238 | describe("the options object:", function() { 239 | 240 | it("listing options", function() { 241 | var doc = TidyDoc(); 242 | var opts = doc.options; 243 | expect(opts).to.containSubset({ 244 | tab_size: 8, 245 | alt_text: null, 246 | }); 247 | expect(opts).to.not.have.any.keys("tab-size", "altText"); 248 | }); 249 | 250 | it("complete list", function() { 251 | var doc = TidyDoc(); 252 | var opts = doc.options; 253 | expect(Object.keys(opts).length).to.be.equal(doc.getOptionList().length); 254 | }); 255 | 256 | it("can be used for getting", function() { 257 | var doc = TidyDoc(); 258 | var opts = doc.options; 259 | expect(opts.alt_text).to.be.null; 260 | doc.optSet("alt-text", "foo"); 261 | expect(opts.alt_text).to.be.equal("foo"); 262 | expect(doc.options.alt_text).to.be.equal("foo"); 263 | }); 264 | 265 | it("can be used for setting", function() { 266 | var doc = TidyDoc(); 267 | expect(doc.optGet("alt-text")).to.be.null; 268 | doc.options.alt_text = "foo"; 269 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 270 | }); 271 | 272 | it("can be assigned to for configuration", function() { 273 | var doc = TidyDoc(); 274 | expect(doc.optGet("alt-text")).to.be.null; 275 | expect(doc.optGet("tab-size")).to.be.equal(8); 276 | doc.options = { 277 | alt_text: "foo", 278 | tabSize: 3 279 | }; 280 | expect(doc.optGet("alt-text")).to.be.equal("foo"); 281 | expect(doc.optGet("tab-size")).to.be.equal(3); 282 | }); 283 | 284 | }); 285 | 286 | }); 287 | -------------------------------------------------------------------------------- /test/doc-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | chai.use(require("chai-subset")); 5 | var expect = chai.expect; 6 | var libtidy = require("../"); 7 | var TidyDoc = libtidy.TidyDoc; 8 | 9 | describe("TidyDoc:", function() { 10 | 11 | var testDoc1 = Buffer('<!DOCTYPE html>\n<html><head></head>\n' + 12 | '<body><p>foo</p></body></html>'); 13 | var testDoc2 = Buffer('<!DOCTYPE html>\n<html><head></head>\n' + 14 | '<body><form><ul><li></form></ul></li>'); 15 | 16 | describe("construction:", function() { 17 | 18 | it("as constructor", function() { 19 | var doc = new TidyDoc(); 20 | expect(doc).instanceof(TidyDoc); 21 | expect(doc.optGet("input-xml")).equal(false); 22 | }); 23 | 24 | it("as function", function() { 25 | var doc = TidyDoc(); 26 | expect(doc).instanceof(TidyDoc); 27 | expect(doc.optGet("input-xml")).equal(false); 28 | }); 29 | 30 | }); 31 | 32 | describe("basic synchroneous operation:", function() { 33 | 34 | it("parse buffer", function() { 35 | var messages = 36 | "line 2 column 7 - Warning: inserting missing 'title' element\n"; 37 | var doc = new TidyDoc(); 38 | expect(doc.getErrorLog()).equal(""); 39 | var res = doc.parseBufferSync(testDoc1); 40 | expect(doc.getErrorLog()).equal(messages); 41 | expect(res).to.equal(messages); 42 | }); 43 | 44 | it("clean and repair", function() { 45 | // Can there be any output during clean and repair? 46 | var messages = ""; 47 | var doc = new TidyDoc(); 48 | doc.parseBufferSync(testDoc1); 49 | var res = doc.cleanAndRepairSync(); 50 | expect(doc.getErrorLog()).equal(messages); 51 | expect(res).to.equal(messages); 52 | }); 53 | 54 | it("diagnostics", function() { 55 | var messages = 56 | 'Info: Document content looks like HTML5\n' + 57 | 'Tidy found 1 warning and 0 errors!\n\n'; 58 | var doc = new TidyDoc(); 59 | doc.parseBufferSync(testDoc1); 60 | doc.cleanAndRepairSync(); 61 | var res = doc.runDiagnosticsSync(); 62 | expect(doc.getErrorLog()).equal(messages); 63 | expect(res).equal(messages); 64 | }); 65 | 66 | it("save to buffer", function() { 67 | var doc = new TidyDoc(); 68 | doc.parseBufferSync(testDoc1); 69 | doc.cleanAndRepairSync(); 70 | doc.runDiagnosticsSync(); 71 | var res = doc.saveBufferSync(); 72 | expect(doc.getErrorLog()).equal(""); 73 | expect(Buffer.isBuffer(res)).ok; 74 | expect(res.toString()).to.match(/<title>.*<\/title>/); 75 | }); 76 | 77 | it("report errors in diagnostics", function() { 78 | var messages = 79 | 'Info: Document content looks like HTML5\n' + 80 | 'Tidy found 5 warnings and 2 errors!\n\n' + 81 | 'This document has errors that must be fixed before\n' + 82 | 'using HTML Tidy to generate a tidied up version.\n\n'; 83 | var doc = new TidyDoc(); 84 | doc.parseBufferSync(testDoc2); 85 | doc.cleanAndRepairSync(); 86 | var res = doc.runDiagnosticsSync(); 87 | expect(doc.getErrorLog()).equal(messages); 88 | expect(res).equal(messages); 89 | }); 90 | 91 | it("will not produce output in case of an error", function() { 92 | var doc = new TidyDoc(); 93 | doc.parseBufferSync(testDoc2); 94 | doc.cleanAndRepairSync(); 95 | doc.runDiagnosticsSync(); 96 | var res = doc.saveBufferSync(); 97 | expect(doc.getErrorLog()).equal(""); 98 | expect(Buffer.isBuffer(res)).ok; 99 | expect(res).to.have.length(0); 100 | }); 101 | 102 | it("can produce output despite errors", function() { 103 | var doc = new TidyDoc(); 104 | doc.parseBufferSync(testDoc2); 105 | doc.cleanAndRepairSync(); 106 | doc.runDiagnosticsSync(); 107 | expect(doc.optGet("force-output")).to.be.false; 108 | doc.optSet("force-output", true); 109 | expect(doc.optGet("force-output")).to.be.true; 110 | var res = doc.saveBufferSync(); 111 | expect(doc.getErrorLog()).equal(""); 112 | expect(res).to.have.length.above(100); 113 | }); 114 | 115 | }); 116 | 117 | describe("basic asynchroneous operation using callback:", function() { 118 | 119 | it("parse buffer", function(done) { 120 | var messages = 121 | "line 2 column 7 - Warning: inserting missing 'title' element\n"; 122 | var doc = new TidyDoc(); 123 | doc.parseBuffer(testDoc1, function(err, res) { 124 | expect(err).to.be.null; 125 | expect(res).to.not.contain.key("output"); 126 | expect(res).to.containSubset({ 127 | errlog: messages, 128 | }); 129 | expect(doc.getErrorLog()).equal(messages); 130 | done(); 131 | }); 132 | }); 133 | 134 | it("clean and repair", function(done) { 135 | // Can there be any output during clean and repair? 136 | var messages = ""; 137 | var doc = new TidyDoc(); 138 | doc.parseBufferSync(testDoc1); 139 | doc.cleanAndRepair(function(err, res) { 140 | expect(err).to.be.null; 141 | expect(res).to.not.contain.key("output"); 142 | expect(res).to.containSubset({ 143 | errlog: messages, 144 | }); 145 | expect(doc.getErrorLog()).equal(messages); 146 | done(); 147 | }); 148 | }); 149 | 150 | it("diagnostics", function(done) { 151 | var messages = 152 | 'Info: Document content looks like HTML5\n' + 153 | 'Tidy found 1 warning and 0 errors!\n\n'; 154 | var doc = new TidyDoc(); 155 | doc.parseBufferSync(testDoc1); 156 | doc.cleanAndRepairSync(); 157 | doc.runDiagnostics(function(err, res) { 158 | expect(err).to.be.null; 159 | expect(res).to.not.contain.key("output"); 160 | expect(res).to.containSubset({ 161 | errlog: messages, 162 | }); 163 | expect(doc.getErrorLog()).equal(messages); 164 | done(); 165 | }); 166 | }); 167 | 168 | it("save to buffer", function(done) { 169 | var doc = new TidyDoc(); 170 | doc.parseBufferSync(testDoc1); 171 | doc.cleanAndRepairSync(); 172 | doc.runDiagnosticsSync(); 173 | doc.saveBuffer(function(err, res) { 174 | expect(err).to.be.null; 175 | expect(res).to.contain.key("output"); 176 | expect(res).to.containSubset({ 177 | errlog: "", 178 | }); 179 | expect(Buffer.isBuffer(res.output)).ok; 180 | expect(res.output.toString()).to.match(/<title>.*<\/title>/); 181 | expect(doc.getErrorLog()).equal(""); 182 | done(); 183 | }); 184 | }); 185 | 186 | it("will not produce output in case of an error", function(done) { 187 | var doc = new TidyDoc(); 188 | doc.parseBufferSync(testDoc2); 189 | doc.cleanAndRepairSync(); 190 | doc.runDiagnosticsSync(); 191 | var res = doc.saveBufferSync(); 192 | doc.saveBuffer(function(err, res) { 193 | expect(err).to.be.null; 194 | expect(res).to.contain.key("output"); 195 | expect(res).to.containSubset({ 196 | errlog: "", 197 | output: null, 198 | }); 199 | expect(doc.getErrorLog()).equal(""); 200 | done(); 201 | }); 202 | }); 203 | 204 | it("all in one go", function(done) { 205 | var doc = new TidyDoc(); 206 | doc.tidyBuffer(testDoc1, function(err, res) { 207 | expect(err).to.be.null; 208 | expect(res).to.contain.key("output"); 209 | expect(res).to.contain.key("errlog"); 210 | expect(res.errlog).to.match(/inserting missing/); 211 | expect(res.errlog).to.match(/looks like HTML5/); 212 | expect(res.errlog).to.match(/Tidy found/); 213 | expect(Buffer.isBuffer(res.output)).ok; 214 | expect(res.output.toString()).to.match(/<title>.*<\/title>/); 215 | done(); 216 | }); 217 | }); 218 | 219 | }); 220 | 221 | describe("basic asynchroneous operation using promise:", function() { 222 | 223 | it("parse buffer", function() { 224 | var messages = 225 | "line 2 column 7 - Warning: inserting missing 'title' element\n"; 226 | var doc = new TidyDoc(); 227 | return doc.parseBuffer(testDoc1).then(function(res) { 228 | expect(res).to.not.contain.key("output"); 229 | expect(res).to.containSubset({ 230 | errlog: messages, 231 | }); 232 | expect(doc.getErrorLog()).equal(messages); 233 | }); 234 | }); 235 | 236 | it("clean and repair", function() { 237 | // Can there be any output during clean and repair? 238 | var messages = ""; 239 | var doc = new TidyDoc(); 240 | doc.parseBufferSync(testDoc1); 241 | return doc.cleanAndRepair().then(function(res) { 242 | expect(res).to.not.contain.key("output"); 243 | expect(res).to.containSubset({ 244 | errlog: messages, 245 | }); 246 | expect(doc.getErrorLog()).equal(messages); 247 | }); 248 | }); 249 | 250 | it("diagnostics", function() { 251 | var messages = 252 | 'Info: Document content looks like HTML5\n' + 253 | 'Tidy found 1 warning and 0 errors!\n\n'; 254 | var doc = new TidyDoc(); 255 | doc.parseBufferSync(testDoc1); 256 | doc.cleanAndRepairSync(); 257 | return doc.runDiagnostics().then(function(res) { 258 | expect(res).to.not.contain.key("output"); 259 | expect(res).to.containSubset({ 260 | errlog: messages, 261 | }); 262 | expect(doc.getErrorLog()).equal(messages); 263 | }); 264 | }); 265 | 266 | it("save to buffer", function() { 267 | var doc = new TidyDoc(); 268 | doc.parseBufferSync(testDoc1); 269 | doc.cleanAndRepairSync(); 270 | doc.runDiagnosticsSync(); 271 | return doc.saveBuffer().then(function(res) { 272 | expect(res).to.contain.key("output"); 273 | expect(res).to.containSubset({ 274 | errlog: "", 275 | }); 276 | expect(Buffer.isBuffer(res.output)).ok; 277 | expect(res.output.toString()).to.match(/<title>.*<\/title>/); 278 | expect(doc.getErrorLog()).equal(""); 279 | }); 280 | }); 281 | 282 | it("will not produce output in case of an error", function() { 283 | var doc = new TidyDoc(); 284 | doc.parseBufferSync(testDoc2); 285 | doc.cleanAndRepairSync(); 286 | doc.runDiagnosticsSync(); 287 | var res = doc.saveBufferSync(); 288 | return doc.saveBuffer().then(function(res) { 289 | expect(res).to.contain.key("output"); 290 | expect(res).to.containSubset({ 291 | errlog: "", 292 | output: null, 293 | }); 294 | expect(doc.getErrorLog()).equal(""); 295 | }); 296 | }); 297 | 298 | it("all in one go", function() { 299 | var doc = new TidyDoc(); 300 | return doc.tidyBuffer(testDoc1).then(function(res) { 301 | expect(res).to.contain.key("output"); 302 | expect(res).to.contain.key("errlog"); 303 | expect(res.errlog).to.match(/inserting missing/); 304 | expect(res.errlog).to.match(/looks like HTML5/); 305 | expect(res.errlog).to.match(/Tidy found/); 306 | expect(Buffer.isBuffer(res.output)).ok; 307 | expect(res.output.toString()).to.match(/<title>.*<\/title>/); 308 | }); 309 | }); 310 | 311 | }); 312 | 313 | }); 314 | -------------------------------------------------------------------------------- /src/doc.cc: -------------------------------------------------------------------------------- 1 | #include "node-libtidy.hh" 2 | #include <string> 3 | #include <sstream> 4 | 5 | namespace node_libtidy { 6 | 7 | Nan::Persistent<v8::Function> Doc::constructor; 8 | 9 | NAN_MODULE_INIT(Doc::Init) { 10 | v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New); 11 | tpl->SetClassName(Nan::New("TidyDoc").ToLocalChecked()); 12 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 13 | 14 | Nan::SetPrototypeMethod(tpl, "parseBufferSync", parseBufferSync); 15 | Nan::SetPrototypeMethod(tpl, "cleanAndRepairSync", cleanAndRepairSync); 16 | Nan::SetPrototypeMethod(tpl, "runDiagnosticsSync", runDiagnosticsSync); 17 | Nan::SetPrototypeMethod(tpl, "saveBufferSync", saveBufferSync); 18 | Nan::SetPrototypeMethod(tpl, "getOptionList", getOptionList); 19 | Nan::SetPrototypeMethod(tpl, "getOption", getOption); 20 | Nan::SetPrototypeMethod(tpl, "optGet", optGet); 21 | Nan::SetPrototypeMethod(tpl, "optSet", optSet); 22 | Nan::SetPrototypeMethod(tpl, "optGetCurrPick", optGetCurrPick); 23 | Nan::SetPrototypeMethod(tpl, "optGetDoc", optGetDoc); 24 | Nan::SetPrototypeMethod(tpl, "optGetDocLinksList", optGetDocLinksList); 25 | Nan::SetPrototypeMethod(tpl, "optResetToDefault", optResetToDefault); 26 | Nan::SetPrototypeMethod(tpl, "_async2", async); 27 | Nan::SetPrototypeMethod(tpl, "getErrorLog", getErrorLog); 28 | 29 | constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked()); 30 | Nan::Set(target, Nan::New("TidyDoc").ToLocalChecked(), 31 | Nan::GetFunction(tpl).ToLocalChecked()); 32 | } 33 | 34 | Doc::Doc() : locked(false) { 35 | doc = tidyCreateWithAllocator(&allocator); 36 | } 37 | 38 | Doc::~Doc() { 39 | tidyRelease(doc); 40 | } 41 | 42 | NAN_METHOD(Doc::New) { 43 | if (info.IsConstructCall()) { 44 | Doc *obj = new Doc(); 45 | obj->Wrap(info.This()); 46 | info.GetReturnValue().Set(info.This()); 47 | } else { 48 | const int argc = 1; 49 | v8::Local<v8::Value> argv[argc] = {info[0]}; 50 | v8::Local<v8::Function> cons = Nan::New(constructor); 51 | info.GetReturnValue().Set 52 | (Nan::NewInstance(cons, argc, argv).ToLocalChecked()); 53 | } 54 | } 55 | 56 | Doc* Doc::Prelude(v8::Local<v8::Object> self) { 57 | Doc* doc = Nan::ObjectWrap::Unwrap<Doc>(self); 58 | if (doc->locked) { 59 | Nan::ThrowError("TidyDoc is locked for asynchroneous use."); 60 | return NULL; 61 | } 62 | doc->err.reset(); 63 | // Error buffer will get turned into a string, so use LF only there 64 | int nl = tidyOptGetInt(doc->doc, TidyNewline); 65 | tidyOptSetInt(doc->doc, TidyNewline, TidyLF); 66 | int rc = tidySetErrorBuffer(doc->doc, doc->err); 67 | tidyOptSetInt(doc->doc, TidyNewline, nl); 68 | if (rc) { 69 | Nan::ThrowError("Error calling tidySetErrorBuffer"); 70 | return NULL; 71 | } 72 | return doc; 73 | }; 74 | 75 | bool Doc::CheckResult(int rc, const char* functionName) { 76 | if (rc < 0) { // Serious problem, probably rc == -errno 77 | std::ostringstream buf; 78 | buf << functionName << " returned " << rc; 79 | if (!err.isEmpty()) 80 | buf << " - " << err; 81 | Nan::ThrowError(NewString(trim(buf.str()))); 82 | err.reset(); 83 | return false; 84 | } 85 | return true; 86 | }; 87 | 88 | NAN_METHOD(Doc::parseBufferSync) { 89 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 90 | if (!node::Buffer::HasInstance(info[0])) { 91 | Nan::ThrowTypeError("Argument to parseBufferSync must be a buffer"); 92 | return; 93 | } 94 | TidyBuffer inbuf; 95 | tidyBufInitWithAllocator(&inbuf, &allocator); 96 | tidyBufAttach(&inbuf, c2b(node::Buffer::Data(info[0])), 97 | node::Buffer::Length(info[0])); 98 | int rc = tidyParseBuffer(doc->doc, &inbuf); 99 | tidyBufDetach(&inbuf); 100 | if (doc->CheckResult(rc, "tidyParseBuffer")) 101 | info.GetReturnValue().Set(doc->err.string().ToLocalChecked()); 102 | } 103 | 104 | NAN_METHOD(Doc::cleanAndRepairSync) { 105 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 106 | int rc = tidyCleanAndRepair(doc->doc); 107 | if (doc->CheckResult(rc, "tidyCleanAndRepair")) 108 | info.GetReturnValue().Set(doc->err.string().ToLocalChecked()); 109 | } 110 | 111 | NAN_METHOD(Doc::runDiagnosticsSync) { 112 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 113 | int rc = tidyRunDiagnostics(doc->doc); 114 | if (doc->CheckResult(rc, "runDiagnosticsSync")) 115 | info.GetReturnValue().Set(doc->err.string().ToLocalChecked()); 116 | } 117 | 118 | NAN_METHOD(Doc::saveBufferSync) { 119 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 120 | Buf out; 121 | int rc = tidySaveBuffer(doc->doc, out); 122 | if (doc->CheckResult(rc, "tidySaveBuffer")) 123 | info.GetReturnValue().Set(out.buffer().ToLocalChecked()); 124 | } 125 | 126 | NAN_METHOD(Doc::getOptionList) { 127 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 128 | v8::Local<v8::Array> arr = Nan::New<v8::Array>(); 129 | TidyIterator iter = tidyGetOptionList(doc->doc); 130 | while (iter) { 131 | TidyOption opt = tidyGetNextOption(doc->doc, &iter); 132 | v8::Local<v8::Object> obj = Opt::Create(opt); 133 | Nan::Set(arr, arr->Length(), obj); 134 | } 135 | info.GetReturnValue().Set(arr); 136 | } 137 | 138 | TidyOption Doc::asOption(v8::Local<v8::Value> key) { 139 | TidyOption opt = NULL; 140 | { 141 | Nan::TryCatch tryCatch; 142 | opt = Opt::Unwrap(key); 143 | if (opt || !tryCatch.CanContinue()) return opt; 144 | } 145 | if (key->IsNumber()) { 146 | double dnum = Nan::To<double>(key).FromJust(); 147 | int inum = dnum; 148 | if (dnum != inum) { 149 | Nan::ThrowTypeError("Option id must be an integer"); 150 | } else if (inum <= 0 || inum > N_TIDY_OPTIONS) { 151 | Nan::ThrowRangeError("Option id outside range of allowed options"); 152 | } else { 153 | opt = tidyGetOption(doc, TidyOptionId(inum)); 154 | if (!opt) { 155 | Nan::ThrowError("No option with this id"); 156 | } 157 | } 158 | return opt; 159 | } 160 | Nan::Utf8String str1(key); 161 | std::string str2(*str1, str1.length()); 162 | for (std::string::size_type i = str2.length() - 1; i > 0; --i) { 163 | if (str2[i] == '_') str2[i] = '-'; 164 | if (str2[i] >= 'A' && str2[i] <= 'Z' && 165 | str2[i - 1] >= 'a' && str2[i - 1] <= 'z') { 166 | str2[i] = str2[i] + ('a' - 'A'); 167 | str2.insert(i, 1, '-'); 168 | } 169 | } 170 | opt = tidyGetOptionByName(doc, str2.c_str()); 171 | if (!opt) { 172 | std::ostringstream buf; 173 | buf << "Option '" << str2 << "' unknown"; 174 | Nan::ThrowError(NewString(buf.str())); 175 | } 176 | return opt; 177 | } 178 | 179 | NAN_METHOD(Doc::getOption) { 180 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 181 | TidyOption opt = doc->asOption(info[0]); 182 | if (!opt) return; 183 | info.GetReturnValue().Set(Opt::Create(opt)); 184 | } 185 | 186 | NAN_METHOD(Doc::optGet) { 187 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 188 | TidyOption opt = doc->asOption(info[0]); 189 | if (!opt) return; 190 | TidyOptionId id = tidyOptGetId(opt); 191 | const char* str; 192 | switch (tidyOptGetType(opt)) { 193 | case TidyBoolean: 194 | info.GetReturnValue().Set(Nan::New<v8::Boolean> 195 | (bb(tidyOptGetBool(doc->doc, id)))); 196 | break; 197 | case TidyInteger: 198 | str = tidyOptGetCurrPick(doc->doc, tidyOptGetId(opt)); 199 | if (str) 200 | info.GetReturnValue().Set(Nan::New<v8::String>(str).ToLocalChecked()); 201 | else 202 | info.GetReturnValue().Set(Nan::New<v8::Number> 203 | (tidyOptGetInt(doc->doc, id))); 204 | break; 205 | default: 206 | str = tidyOptGetValue(doc->doc, id); 207 | if (str) 208 | info.GetReturnValue().Set(Nan::New<v8::String>(str).ToLocalChecked()); 209 | else 210 | info.GetReturnValue().Set(Nan::Null()); 211 | } 212 | } 213 | 214 | NAN_METHOD(Doc::optSet) { 215 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 216 | TidyOption opt = doc->asOption(info[0]); 217 | if (!opt) return; 218 | if (tidyOptIsReadOnly(opt) != no) { 219 | std::ostringstream buf; 220 | buf << "Option '" << tidyOptGetName(opt) << "' is readonly"; 221 | Nan::ThrowError(NewString(buf.str())); 222 | return; 223 | } 224 | TidyOptionId id = tidyOptGetId(opt); 225 | v8::Local<v8::Value> val = info[1]; 226 | Bool rc; 227 | if (val->IsBoolean() && tidyOptGetType(opt) == TidyBoolean) { 228 | rc = tidyOptSetBool(doc->doc, id, bb(Nan::To<bool>(info[1]).FromJust())); 229 | } else if (val->IsNumber() && tidyOptGetType(opt) == TidyInteger) { 230 | rc = tidyOptSetInt(doc->doc, id, Nan::To<double>(info[1]).FromJust()); 231 | } else if (val->IsNull() || val->IsUndefined()) { 232 | rc = tidyOptSetValue(doc->doc, id, ""); 233 | } else { 234 | Nan::Utf8String str(val); 235 | rc = tidyOptSetValue(doc->doc, id, *str); 236 | } 237 | if (rc != yes) { 238 | std::ostringstream buf; 239 | buf << "Failed to set option '" << tidyOptGetName(opt) 240 | << "' to value '" << Nan::Utf8String(info[1]) << "'"; 241 | if (!doc->err.isEmpty()) { 242 | buf << " - " << doc->err; 243 | } 244 | Nan::ThrowError(NewString(trim(buf.str()))); 245 | } 246 | } 247 | 248 | NAN_METHOD(Doc::optGetCurrPick) { 249 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 250 | TidyOption opt = doc->asOption(info[0]); 251 | if (!opt) return; 252 | const char* res = tidyOptGetCurrPick(doc->doc, tidyOptGetId(opt)); 253 | if (res) 254 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 255 | else 256 | info.GetReturnValue().Set(Nan::Null()); 257 | } 258 | 259 | NAN_METHOD(Doc::optGetDoc) { 260 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 261 | TidyOption opt = doc->asOption(info[0]); 262 | if (!opt) return; 263 | const char* res = tidyOptGetDoc(doc->doc, opt); 264 | if (res) 265 | info.GetReturnValue().Set(Nan::New<v8::String>(res).ToLocalChecked()); 266 | else 267 | info.GetReturnValue().Set(Nan::Null()); 268 | } 269 | 270 | NAN_METHOD(Doc::optGetDocLinksList) { 271 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 272 | TidyOption opt = doc->asOption(info[0]); 273 | if (!opt) return; 274 | v8::Local<v8::Array> arr = Nan::New<v8::Array>(); 275 | TidyIterator iter = tidyOptGetDocLinksList(doc->doc, opt); 276 | while (iter) { 277 | TidyOption opt = tidyOptGetNextDocLinks(doc->doc, &iter); 278 | v8::Local<v8::Object> obj = Opt::Create(opt); 279 | Nan::Set(arr, arr->Length(), obj); 280 | } 281 | info.GetReturnValue().Set(arr); 282 | } 283 | 284 | NAN_METHOD(Doc::optResetToDefault) { 285 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 286 | TidyOption opt = doc->asOption(info[0]); 287 | if (!opt) return; 288 | tidyOptResetToDefault(doc->doc, tidyOptGetId(opt)); 289 | } 290 | 291 | // arguments: 292 | // 0 - input buffer or null if already parsed 293 | // 1 - boolean whether to call tidyCleanAndRepair 294 | // 2 - boolean whether to call tidyRunDiagnostics 295 | // 3 - boolean whether to save the output to a buffer 296 | // 4 - resolve callback to invoke once we are done successfully 297 | // 5 - reject callback to invoke if there was an error 298 | NAN_METHOD(Doc::async) { 299 | Doc* doc = Prelude(info.Holder()); if (!doc) return; 300 | if (info.Length() != 6) { 301 | Nan::ThrowTypeError("_async2 must be called with exactly 6 arguments."); 302 | return; 303 | } 304 | if (!(info[0]->IsNull() || node::Buffer::HasInstance(info[0]))) { 305 | Nan::ThrowTypeError("First argument to _async2 must be a buffer"); 306 | return; 307 | } 308 | if (!info[4]->IsFunction()) { 309 | Nan::ThrowTypeError("Resolve argument to _async2 must be a function"); 310 | return; 311 | } 312 | if (!info[5]->IsFunction()) { 313 | Nan::ThrowTypeError("Reject argument to _async2 must be a function"); 314 | return; 315 | } 316 | TidyWorker* w = new TidyWorker(doc, 317 | info[4].As<v8::Function>(), 318 | info[5].As<v8::Function>()); 319 | w->SaveToPersistent(0u, info.Holder()); 320 | if (!info[0]->IsNull()) { 321 | w->SaveToPersistent(1u, info[0]); 322 | w->setInput(node::Buffer::Data(info[0]), node::Buffer::Length(info[0])); 323 | } 324 | w->shouldCleanAndRepair = Nan::To<bool>(info[1]).FromJust(); 325 | w->shouldRunDiagnostics = Nan::To<bool>(info[2]).FromJust(); 326 | w->shouldSaveToBuffer = Nan::To<bool>(info[3]).FromJust(); 327 | Nan::AsyncQueueWorker(w); 328 | } 329 | 330 | NAN_METHOD(Doc::getErrorLog) { 331 | Doc* doc = Nan::ObjectWrap::Unwrap<Doc>(info.Holder()); 332 | if (doc->locked) { 333 | Nan::ThrowError("TidyDoc is locked for asynchroneous use."); 334 | return; 335 | } 336 | info.GetReturnValue().Set(doc->err.string().ToLocalChecked()); 337 | } 338 | 339 | } 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libtidy 2 | 3 | This package provides bindings to 4 | [libtidy](http://www.html-tidy.org/developer/) 5 | which can be used to parse and tidy up HTML 5. 6 | The library is built as a native node extension, 7 | compiled from sources shipped with the package. 8 | Precompiled libraries are available for many common platforms, 9 | and will get installed automatically if available. 10 | So you don't have to have the HTML Tidy package installed on your system. 11 | 12 | ## Alternatives 13 | 14 | * [tidy-html5](https://www.npmjs.com/package/tidy-html5) 15 | has libtidy compiled to JavaScript using 16 | [emscripten](http://emscripten.org/). 17 | It is likely more portable, but at the cost of performance. 18 | Only supports synchroneous operation. 19 | * [tidy](https://www.npmjs.com/package/tidy) 20 | and [tidy2](https://www.npmjs.com/package/tidy2) 21 | also provide bindings for libtidy, 22 | but they expect the library and its header files 23 | to be installed on the system. 24 | Only supports synchroneous operation. 25 | * [htmltidy](https://www.npmjs.com/package/htmltidy) 26 | and [htmltidy2](https://www.npmjs.com/package/htmltidy2) 27 | use the command line tool to tidy up html, 28 | so they incur some process creation overhead. 29 | The binaries for the most common platforms are shipped with the package, 30 | but other platforms are not supported. 31 | This approach requires no build tools, though. 32 | Only supports asynchroneous operation. 33 | 34 | The project will try to provide drop-in replacements for these libraries, 35 | so that people can easily compare implementations. 36 | At the moment, the `tidy` method shared with the `htmltidy` modules 37 | is the only such replacement which has been implemented. 38 | 39 | ## Usage 40 | 41 | The project aims to provide fine-grained access to a growing set of 42 | library functions, with a rather direct mapping between JavaScript and 43 | C functions. 44 | On the other hand, the project offers high-level functions to easily 45 | deal with common workflows. 46 | 47 | ### Callback convention 48 | 49 | Most asynchroneous operations in this library take an optional callback 50 | with the conventional node signature `cb(err, res)`. 51 | If no callback is provided, a promise is returned 52 | which either resolves to `res` or gets rejected with `err`. 53 | In the case of a serious error, `err` will contain an exception 54 | providing details about the problem. 55 | In less severe situations, `err` will be `null` (in case of a callback) 56 | and `res` will be an object containing several properties: 57 | 58 | * **`errlog`** contains the error messages generated during the run, 59 | formatted as a string including a trailing newline. 60 | * **`output`** contains the output buffer if output was generated. 61 | The property is unset if generating output was not part of the 62 | method in question, or `null` if no output was generated due to errors. 63 | 64 | Other useful properties may be added in the future. 65 | 66 | ### High-level 67 | 68 | High-level functions automate the most common workflows. 69 | 70 | #### tidyBuffer(document, [options,] callback) 71 | 72 | The `document` is assumed to be a buffer or a string. 73 | Anything else will be converted to a string and then turned into a buffer. 74 | `options` is an optional dictionary of options, 75 | see [the section on options](#options) for details. 76 | `callback` follows the [convention described above](#callback-convention). 77 | 78 | ### Basic workflow 79 | 80 | The type `libtidy.TidyDoc` is the central object for dealing with the 81 | library at a low level. 82 | Such an object will hold a configuration and be able to process one 83 | input file at a time, while multiple such objects can deal with 84 | multiple inputs simultaneously using an independent configuration for 85 | each of them. 86 | 87 | The basic workflow consists of these four steps executed on such an object: 88 | 89 | | Step | C API | Synchroneous JavaScript | Asynchroneous JavaScript | 90 | | --- | --- | --- | --- | 91 | | 1. | [`tidyParseBuffer(doc,&buf)`][tidyParseBuffer] | `doc.parseBufferSync(buf)` | `doc.parseBuffer(buf,cb)` | 92 | | 2. | [`tidyCleanAndRepair(doc)`][tidyCleanAndRepair] | `doc.cleanAndRepairSync()` | `doc.cleanAndRepair(cb)` | 93 | | 3. | [`tidyRunDiagnostics(doc)`][tidyRunDiagnostics] | `doc.runDiagnosticsSync()` | `doc.runDiagnostics(cb)` | 94 | | 4. | [`tidySaveBuffer(doc,&buf)`][tidySaveBuffer] | `doc.saveBufferSync()` | `doc.saveBuffer(cb)` | 95 | 96 | Most synchroneous functions take no argument 97 | and return any diagnostic messages generated in the process. 98 | The first of the methods takes a buffer as an argument, 99 | and the last returns the resulting output buffer. 100 | The asynchroneous methods take a callback function as last argument, 101 | following the [convention described above](#callback-convention). 102 | 103 | ### Options 104 | 105 | For the list of available options, please refer to the 106 | [Quick Reference][quick_ref]. 107 | 108 | There are various ways to operate on options. 109 | Each time an option is identified, the library offers several choices: 110 | the option may be identified by name (i.e. a string), 111 | by id (i.e. an integer) or by a `TidyOption` object. 112 | When using a string, you may choose between the original hyphenated name, 113 | a version where hyphens are replaced by underscores, or a camelCase version. 114 | So `alt-text`, `alt_text` and `altText` all describe the same option. 115 | 116 | The lowest level of option access are the `optGet(key)` and 117 | `optSet(key, value)` methods of the `TidyDoc` object. 118 | These encompass the whole `tidyOpt{Get,Set}{Value,Int,Bool}` 119 | family of functions in the C API. 120 | 121 | The methods `getOption(key)` and `getOptionList()` return a single 122 | `TidyOption` object resp. the list of all available options. 123 | Each such option object contains getters for the following properties: 124 | `name`, `category`, `id`, `type`, `readOnly`, `default`, `pickList`. 125 | 126 | The `options` property of each `TidyDoc` object can be used for elegant 127 | high-level access to all the options. 128 | It provides a dictionary of getter/setter pairs, 129 | which can be used to directly inspect modify each of the options. 130 | The keys in this dictionary use the underscore notation. 131 | The `options` property itself is implemented using a getter/setter pair, 132 | and the setter takes its argument and configures all its keys-value pairs. 133 | In this case you again have full choice of naming convention. 134 | So one way to configure a document object would be this: 135 | 136 | ```js 137 | var libtidy = require("libtidy"); 138 | var doc = libtidy.TidyDoc(); 139 | doc.options = { 140 | forceOutput = true, 141 | output_xhtml = false, 142 | }; 143 | ``` 144 | 145 | ## API 146 | 147 | The following lists the full public interface of the package. 148 | Details on each item can be found in the 149 | [API documentation](https://github.com/gagern/node-libtidy/blob/master/API.md). 150 | 151 | - [**tidyBuffer(input, [opts], [cb])**][APItidyBuffer] – async function 152 | - [**TidyDoc()**][APITidyDoc] – constructor 153 | - [**cleanAndRepair([cb])**][APIcleanAndRepair] – async method 154 | - [**cleanAndRepairSync()**][APIcleanAndRepairSync] – method 155 | - [**getOption(key)**][APIgetOption] – method 156 | - [**getOptionList()**][APIgetOptionList] – method 157 | - [**optGet(key)**][APIoptGet] – method 158 | - [**optGetCurrPick(key)**][APIoptGetCurrPick] – method 159 | - [**optGetDoc(key)**][APIoptGetDoc] – method 160 | - [**optGetDocLinksList(key)**][APIoptGetDocLinksList] – method 161 | - [**optSet(key, value)**][APIoptSet] – method 162 | - [**options**][APIoptions] – getter and setter 163 | - [**parseBuffer(buf, [cb])**][APIparseBuffer] – async method 164 | - [**parseBufferSync(buf)**][APIparseBufferSync] – method 165 | - [**runDiagnostics([cb])**][APIrunDiagnostics] – async method 166 | - [**runDiagnosticsSync()**][APIrunDiagnosticsSync] – method 167 | - [**saveBuffer([cb])**][APIsaveBuffer] – async method 168 | - [**saveBufferSync()**][APIsaveBufferSync] – method 169 | - [**tidyBuffer(buf, [cb])**][APItidyBuffer] – async method 170 | - [**TidyOption()**][APITidyOption] – constructor (not for public use) 171 | - [**category**][APIcategory] – getter 172 | - [**default**][APIdefault] – getter 173 | - [**id**][APIid] – getter 174 | - [**name**][APIname] – getter 175 | - [**pickList**][APIpickList] – getter 176 | - [**readOnly**][APIreadOnly] – getter 177 | - [**toString()**][APItoString] – method 178 | - [**type**][APItype] – getter 179 | - [**compat**][APIcompat] – namespace 180 | - [**htmltidy**][APIhtmltidy] – namespace 181 | - [**tidy(input, [opts], cb)**][APItidy] – async function 182 | 183 | [APItidyBuffer]: https://github.com/gagern/node-libtidy/blob/master/API.md#tidyBuffer 184 | [APITidyDoc]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc 185 | [APIcleanAndRepair]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.cleanAndRepair 186 | [APIcleanAndRepairSync]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.cleanAndRepairSync 187 | [APIgetOption]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.getOption 188 | [APIgetOptionList]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.getOptionList 189 | [APIoptGet]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.optGet 190 | [APIoptGetCurrPick]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.optGetCurrPick 191 | [APIoptGetDoc]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.optGetDoc 192 | [APIoptGetDocLinksList]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.optGetDocLinksList 193 | [APIoptSet]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.optSet 194 | [APIoptions]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.options 195 | [APIparseBuffer]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.parseBuffer 196 | [APIparseBufferSync]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.parseBufferSync 197 | [APIrunDiagnostics]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.runDiagnostics 198 | [APIrunDiagnosticsSync]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.runDiagnosticsSync 199 | [APIsaveBuffer]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.saveBuffer 200 | [APIsaveBufferSync]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.saveBufferSync 201 | [APItidyBuffer]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyDoc.tidyBuffer 202 | [APITidyOption]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption 203 | [APIcategory]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.category 204 | [APIdefault]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.default 205 | [APIid]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.id 206 | [APIname]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.name 207 | [APIpickList]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.pickList 208 | [APIreadOnly]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.readOnly 209 | [APItoString]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.toString 210 | [APItype]: https://github.com/gagern/node-libtidy/blob/master/API.md#TidyOption.type 211 | [APIcompat]: https://github.com/gagern/node-libtidy/blob/master/API.md#compat 212 | [APIhtmltidy]: https://github.com/gagern/node-libtidy/blob/master/API.md#htmltidy 213 | [APItidy]: https://github.com/gagern/node-libtidy/blob/master/API.md#compat.htmltidy.tidy 214 | 215 | ## TypeScript support 216 | 217 | This library ships with a `.d.ts` declaration for TypeScript users. 218 | 219 | For contributors: part of the declaration is generated by `util/gen-typescript-decl.js` (executed by npm `prepublish` / `pretest` hook). 220 | 221 | ## License 222 | 223 | The project itself uses [the MIT license](LICENSE.md). 224 | For the license of the underlying library, please see 225 | [its license file][upstream-license] 226 | 227 | ## Contributing 228 | 229 | To clone the project and start developing run the following commands 230 | 231 | ```sh 232 | git clone --recursive https://github.com/gagern/node-libtidy.git 233 | cd node-libtidy 234 | npm install 235 | npm test 236 | ``` 237 | 238 | If you want to update to the latest version of libtidy, you can execute 239 | 240 | ```sh 241 | cd tidy-html5 242 | git checkout master 243 | echo "Bump libtidy to `git describe --tags`" | tee ../commit_message.tmp 244 | cd .. 245 | git add tidy-html5 246 | npm install 247 | npm test 248 | git commit -e -F commit_message.tmp 249 | rm commit_message.tmp 250 | ``` 251 | 252 | You may want to substitute some other branch name instead of `master`, 253 | e.g. in order to get the latest version of some release branch. 254 | 255 | If you think that using a given version is important not just for yourself, 256 | but for others as well, then please open an issue and request a release 257 | using that version. 258 | If the version in question is not the latest release, 259 | then please provide some reason why that particular version would be useful. 260 | 261 | [tidyParseBuffer]: http://api.html-tidy.org/tidy/tidylib_api_5.4.0/group__Parse.html#gaa28ce34c95750f150205843885317851 262 | [tidyCleanAndRepair]: http://api.html-tidy.org/tidy/tidylib_api_5.4.0/group__Clean.html#ga11fd23eeb4acfaa0f9501effa0c21269 263 | [tidyRunDiagnostics]: http://api.html-tidy.org/tidy/tidylib_api_5.4.0/group__Clean.html#ga6170500974cc02114f6e4a29d44b7d77 264 | [tidySaveBuffer]: http://api.html-tidy.org/tidy/tidylib_api_5.4.0/group__Save.html#ga7e8642262c8c4d34cf7cc426647d29f0 265 | [quick_ref]: http://api.html-tidy.org/tidy/quickref_5.4.0.html 266 | [upstream-license]: https://github.com/htacg/tidy-html5/blob/5.4.0/README/LICENSE.md 267 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API of the libtidy package for node 2 | 3 | General usage: 4 | 5 | ```js 6 | var libtidy = require("libtidy"); 7 | ``` 8 | 9 | <a id="tidyBuffer"></a> 10 | ## tidyBuffer(input, [opts], [cb]) 11 | 12 | Asynchronous function. 13 | Suggested entry point for most applications. 14 | 15 | * **input** – anything except a buffer will be 16 | converted to String and then turned into a buffer. 17 | * **opts** – a dictionary of [libtidy options](README.md#options). 18 | * **cb** – callback following the 19 | [callback convention](README.md#callback-convention), 20 | i.e. with signature `function(exception, {output, errlog})` 21 | or omitted to return a promise. 22 | 23 | The function applies the following libtidy options by default: 24 | 25 | * newline = LF 26 | 27 | <a id="TidyDoc"></a> 28 | ## TidyDoc() 29 | 30 | Constructor. Main entry point for low-level access to the library. 31 | Contruction wraps `tidyCreateWithAllocator`, 32 | while garbage collection triggers `tidyRelease`. 33 | 34 | <a id="TidyDoc.cleanAndRepair"></a> 35 | ### TidyDoc.cleanAndRepair([cb]) 36 | 37 | Asynchronous method binding `tidyCleanAndRepair`. 38 | 39 | * **cb** – callback following the 40 | [callback convention](README.md#callback-convention), 41 | i.e. with signature `function(exception, {errlog})` 42 | or omitted to return a promise. 43 | 44 | <a id="TidyDoc.cleanAndRepairSync"></a> 45 | ### TidyDoc.cleanAndRepairSync() 46 | 47 | Synchronous method binding `tidyCleanAndRepair`. 48 | Returns any diagnostics encountered during operation, as a string. 49 | 50 | <a id="TidyDoc.getOption"></a> 51 | ### TidyDoc.getOption(key) 52 | 53 | Retrieve the [TidyOption](#TidyOption) object for the key in question. 54 | 55 | * **key** – can be a string 56 | (hyphen-separated, underscore_separated or camelCase), 57 | a numeric [id](#TidyOption.id) (not portable) 58 | or a [TidyOption](#TidyOption) object (rather pointless here). 59 | 60 | Wraps `tidyGetOptionByName` and `tidyGetOption`. 61 | 62 | <a id="TidyDoc.getOptionList"></a> 63 | ### TidyDoc.getOptionList() 64 | 65 | Retrieve the list of all known options, 66 | as an array of [TidyOption](#TidyOption) objects. 67 | 68 | Wraps `tidyGetOptionList` and `tidyGetNextOption`. 69 | 70 | <a id="TidyDoc.optGet"></a> 71 | ### TidyDoc.optGet(key) 72 | 73 | Get the current value of the option in question. 74 | 75 | For enumerated options, 76 | the result will return the [current pick value](#TidyDoc.optGetCurrPick) 77 | as a string. 78 | Otherwise, `optGet` returns the option value based on its 79 | [type](#TidyOption.type), 80 | i.e. call `tidyOptGetBool`, `tidyOptGetInt` or `tidyOptGetValue`. 81 | Empty strings will be returned as `null` like libtidy does. 82 | 83 | * **key** – can be a string 84 | (hyphen-separated, underscore_separated or camelCase), 85 | a numeric [id](#TidyOption.id) (not portable) 86 | or a [TidyOption](#TidyOption) object. 87 | 88 | <a id="TidyDoc.optGetCurrPick"></a> 89 | ### TidyDoc.optGetCurrPick(key) 90 | 91 | Get a string representation of the current value for a given option. 92 | The option must have a [pick list](#TidyOption.pickList) associated. 93 | It applies to enumerated options (which internally are of type int) 94 | and to boolean options. 95 | 96 | For most applications, [optGet](#TidyDoc.optGet) should be more suitable. 97 | 98 | * **key** – can be a string 99 | (hyphen-separated, underscore_separated or camelCase), 100 | a numeric [id](#TidyOption.id) (not portable) 101 | or a [TidyOption](#TidyOption) object. 102 | 103 | Wraps `tidyOptGetCurrPick`. 104 | 105 | <a id="TidyDoc.optGetDoc"></a> 106 | ### TidyDoc.optGetDoc(key) 107 | 108 | Describe the named option. 109 | The description is a HTML snippet, returned as a string. 110 | 111 | * **key** – can be a string 112 | (hyphen-separated, underscore_separated or camelCase), 113 | a numeric [id](#TidyOption.id) (not portable) 114 | or a [TidyOption](#TidyOption) object. 115 | 116 | Wraps `tidyOptGetDoc`. 117 | 118 | <a id="TidyDoc.optGetDocLinksList"></a> 119 | ### TidyDoc.optGetDocLinksList(key) 120 | 121 | Identify related options. 122 | The result is a (possibly empty) array of [TidyOption](#TidyOption) objects. 123 | 124 | * **key** – can be a string 125 | (hyphen-separated, underscore_separated or camelCase), 126 | a numeric [id](#TidyOption.id) (not portable) 127 | or a [TidyOption](#TidyOption) object. 128 | 129 | Wraps `tidyOptGetDocLinksList` and `tidyOptGetNextDocLinks`. 130 | 131 | <a id="TidyDoc.optSet"></a> 132 | ### TidyDoc.optSet(key, value) 133 | 134 | Set the value for the option in question. 135 | 136 | * **key** – can be a string 137 | (hyphen-separated, underscore_separated or camelCase), 138 | a numeric [id](#TidyOption.id) (not portable) 139 | or a [TidyOption](#TidyOption) object. 140 | * **value** – new value of the option. 141 | 142 | The interpretation of the `value` depends on the 143 | [type](#TidyOption.type) of the option: 144 | 145 | * If the type of the option is integer and the provided value is a number, 146 | the method will call `tidyOptSetInt`. 147 | * If the type of the option is boolean and the provided value is boolean, 148 | the method will call `tidyOptSetBool`. 149 | * If the value is `null`, 150 | `tidyOptSetValue` will be called with an empty string. 151 | * In all other cases, it calls `tidyOptSetValue`, 152 | which in turn uses the libtidy option parser. 153 | This in particular allows parsing enumerated options. 154 | The passed value is the result of JavaScript conversion to string. 155 | One effect of this is that it is possible to pass a 156 | boolean value to an AutoBool option and obtain the expected result. 157 | 158 | Note that `tidyOptSetValue` may not reject all values you'd expect it would. 159 | For example, boolean options are judged by their first non-whitespace letter, 160 | and accept the `auto` keyword just like options of type AutoBool. 161 | 162 | <a id="TidyDoc.options"></a> 163 | ### TidyDoc.options 164 | 165 | Getter and setter pair. 166 | 167 | The getter will build a configuration dictionary 168 | of [all options](#TidyDoc.getOptionList). 169 | Each element of the dictionary corresponds to one option, 170 | with the key given in underscore_separated format. 171 | Reading that element implies a call to [optGet](#TidyDoc.optGet), 172 | while writing it corresponds to calling [optSet](#TidyDoc.optSet). 173 | 174 | The setter will take the assigned value and merge its elements 175 | into the configuration one at a time. 176 | This merging goes through [optSet](#TidyDoc.optSet), 177 | so keys can use any of the allowed option naming schemes. 178 | 179 | <a id="TidyDoc.parseBuffer"></a> 180 | ### TidyDoc.parseBuffer(buf, [cb]) 181 | 182 | Asynchronous method binding `tidyParseBuffer`. 183 | Callback follows the [callback convention](README.md#callback-convention), 184 | i.e. have signature `function(exception, {errlog})` 185 | 186 | * **buf** – must be a buffer, other input will be rejected. 187 | * **cb** – callback following the 188 | [callback convention](README.md#callback-convention), 189 | i.e. with signature `function(exception, {errlog})` 190 | or omitted to return a promise. 191 | 192 | It is suggested to use this method for strings as well, 193 | since JavaScript strings come with a length information 194 | and may contain `\0` characters while C strings are null-terminated. 195 | Make sure to leave the `input-encoding` option at its default of UTF8 196 | if the input is `Buffer(str)`. 197 | 198 | <a id="TidyDoc.parseBufferSync"></a> 199 | ### TidyDoc.parseBufferSync(buf) 200 | 201 | Synchronous method binding `tidyParseBuffer`. 202 | Returns any diagnostics encountered during operation, as a string. 203 | 204 | * **buf** – must be a buffer, other input will be rejected. 205 | 206 | It is suggested to use this method for strings as well, 207 | since JavaScript strings come with a length information 208 | and may contain `\0` characters while C strings are null-terminated. 209 | Make sure to leave the `input-encoding` option at its default of UTF8 210 | if the input is `Buffer(str)`. 211 | 212 | <a id="TidyDoc.runDiagnostics"></a> 213 | ### TidyDoc.runDiagnostics([cb]) 214 | 215 | Asynchronous method binding `tidyRunDiagnostics`. 216 | Callback follows the [callback convention](README.md#callback-convention), 217 | i.e. have signature `function(exception, {errlog})` 218 | or is omitted to return a promise. 219 | 220 | <a id="TidyDoc.runDiagnosticsSync"></a> 221 | ### TidyDoc.runDiagnosticsSync() 222 | 223 | Synchronous method binding `tidyRunDiagnostics`. 224 | Returns any diagnostics encountered during operation, as a string. 225 | 226 | <a id="TidyDoc.saveBuffer"></a> 227 | ### TidyDoc.saveBuffer([cb]) 228 | 229 | Asynchronous method binding `tidySaveBuffer`. 230 | 231 | * **cb** – callback following the 232 | [callback convention](README.md#callback-convention), 233 | i.e. with signature `function(exception, {errlog, output})` 234 | where `output` is a buffer, 235 | or omitted to return a promise. 236 | 237 | <a id="TidyDoc.saveBufferSync"></a> 238 | ### TidyDoc.saveBufferSync() 239 | 240 | Synchronous method binding `tidySaveBuffer`. 241 | Returns the resulting buffer as a string. 242 | 243 | <a id="TidyDoc.tidyBuffer"></a> 244 | ### TidyDoc.tidyBuffer(buf, [cb]) 245 | 246 | Asynchronous method performing the four basic steps in a row: 247 | 248 | 1. `tidyParseBuffer` 249 | 2. `tidyCleanAndRepair` 250 | 3. `tidyRunDiagnostics` 251 | 4. `tidySaveBuffer` 252 | 253 | * **buf** – must be a buffer, other input will be rejected. 254 | * **cb** – callback following the 255 | [callback convention](README.md#callback-convention), 256 | i.e. with signature `function(exception, {errlog, output})` 257 | where `output` is a buffer, or omitted to return a promise. 258 | 259 | <a id="TidyOption"></a> 260 | ## TidyOption() 261 | 262 | Although part of the public interface, 263 | this constructor is not meant to be called from JavaScript. 264 | Its prototype is open to extension, though. 265 | 266 | One can obtain objects of this class from 267 | [TidyDoc.getOption](#TidyDoc.getOption), 268 | [TidyDoc.getOptionList](#TidyDoc.getOptionList) or 269 | [TidyDoc.optGetDocLinksList](#TidyDoc.optGetDocLinksList). 270 | 271 | The properties of this type are implemented as getters, 272 | so actually retrieving these values may incur some performance cost, 273 | and retrieving the same property twice may lead to different 274 | but equivalent objects. 275 | 276 | <a id="TidyOption.category"></a> 277 | ### TidyOption.category 278 | 279 | The category of the option, as a string. 280 | Will be one of `"Markup"`, `"Diagnostics"`, `"PrettyPrint"`, 281 | `"Encoding"` or `"Miscellaneous"`. 282 | 283 | Wraps `tidyOptGetCategory`. 284 | 285 | <a id="TidyOption.default"></a> 286 | ### TidyOption.default 287 | 288 | The default value of the option. 289 | The returned type depends on the type of the option. 290 | Contrary to [`optGet`](#TidyDoc.optGet), 291 | the default will be a number for enumerated types, 292 | not a string, since `tidyOptGetCurrPick` has no 293 | matching `tidyOptGetDefaultPick` or similar. 294 | 295 | Wraps `tidyOptGetDefaultBool`, `tidyOptGetDefaultInt` 296 | or `tidyOptGetDefault`. 297 | 298 | <a id="TidyOption.id"></a> 299 | ### TidyOption.id 300 | 301 | The numeric identifier identifying this option. 302 | The returned number is not portable across different versions, 303 | different builds and perhaps even different machines. 304 | So be careful using this, preferably only within a single process. 305 | In general, the [name](#TidyOption.name) is preferable. 306 | 307 | Wraps `tidyOptGetId`. 308 | 309 | <a id="TidyOption.name"></a> 310 | ### TidyOption.name 311 | 312 | The name of the option, in its hyphen-separated default form. 313 | 314 | Wraps `tidyOptGetName`. 315 | 316 | <a id="TidyOption.pickList"></a> 317 | ### TidyOption.pickList 318 | 319 | List of possible values for enumerated properties, 320 | as an array of strings. 321 | Otherwise the list will be empty. 322 | 323 | Wraps `tidyOptGetPickList` and `tidyOptGetNextPick`. 324 | 325 | <a id="TidyOption.readOnly"></a> 326 | ### TidyOption.readOnly 327 | 328 | Indicates whether the property is read-only. 329 | 330 | Wraps `tidyOptIsReadOnly`. 331 | 332 | <a id="TidyOption.toString"></a> 333 | ### TidyOption.toString() 334 | 335 | Returns the [name](#TidyOption.name) of the option. 336 | 337 | <a id="TidyOption.type"></a> 338 | ### TidyOption.type 339 | 340 | Returns the type of the option, as a string. 341 | Will be one of `"boolean"`, `"integer"`, `"string"`. 342 | Note that enumerated options, including those of type AutoBool, 343 | will be represented as type `"integer"`. 344 | 345 | Wraps `tidyOptGetType`. 346 | 347 | <a id="compat"></a> 348 | ## compat 349 | 350 | Elements of the `compat` namespace offer 351 | compatibility with other libtidy bindings for node. 352 | 353 | <a id="htmltidy"></a> 354 | ### compat.htmltidy 355 | 356 | This offers a drop-in replacement for 357 | [htmltidy](https://www.npmjs.com/package/htmltidy) and 358 | [htmltidy2](https://www.npmjs.com/package/htmltidy2). 359 | 360 | ```diff 361 | -var htmltidy = require("htmltidy2"); 362 | +var htmltidy = require("libtidy").compat.htmltidy; 363 | ``` 364 | 365 | <a id="compat.htmltidy.tidy"></a> 366 | <a id="tidy"></a> 367 | ## compat.htmltidy.tidy(input, [opts], cb) 368 | 369 | Asynchronous function. 370 | 371 | Similar to [`tidyBuffer`](#tidyBuffer), 372 | but the arguments to the callback are different. 373 | In case of a non-serious error, 374 | the first argument will contain any diagnostic messages as a string, 375 | while the second argument holds the output, again as a string. 376 | If the `show-warnings` option is false 377 | (which is the default for this function), 378 | then in case of a successfully generated output 379 | an empty diagnostics string will be returned. 380 | 381 | * **input** – anything except a buffer will be 382 | converted to String and then turned into a buffer. 383 | * **opts** – a dictionary of [libtidy options](README.md#options). 384 | * **cb** – callback with signature `function(err, output)`, 385 | where `err` is an `Error` in case of a serious error, 386 | or a diagnostic string in case of less serious problems. 387 | 388 | The function applies the following libtidy options by default: 389 | 390 | * show-warnings = no 391 | * tidy-mark = no 392 | * force-output = yes 393 | * quiet = no 394 | --------------------------------------------------------------------------------