{"id":13660182,"url":"https://github.com/marko-js/htmljs-parser","last_synced_at":"2025-04-08T08:17:50.008Z","repository":{"id":37773960,"uuid":"41948146","full_name":"marko-js/htmljs-parser","owner":"marko-js","description":"An HTML parser recognizes content and string placeholders and allows JavaScript expressions as attribute values","archived":false,"fork":false,"pushed_at":"2025-02-20T01:59:19.000Z","size":2512,"stargazers_count":141,"open_issues_count":4,"forks_count":20,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-03-23T13:01:54.802Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marko-js.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-09-05T03:51:42.000Z","updated_at":"2025-02-20T01:55:58.000Z","dependencies_parsed_at":"2025-04-01T05:38:12.511Z","dependency_job_id":"d962723d-c387-4182-9a55-4f9df4166ad8","html_url":"https://github.com/marko-js/htmljs-parser","commit_stats":{"total_commits":484,"total_committers":15,"mean_commits":"32.266666666666666","dds":0.5082644628099173,"last_synced_commit":"b1057180986957ad525000281e08468f5405ff16"},"previous_names":["philidem/htmljs-parser"],"tags_count":108,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marko-js%2Fhtmljs-parser","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marko-js%2Fhtmljs-parser/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marko-js%2Fhtmljs-parser/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marko-js%2Fhtmljs-parser/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marko-js","download_url":"https://codeload.github.com/marko-js/htmljs-parser/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247801176,"owners_count":20998339,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T05:01:17.991Z","updated_at":"2025-04-08T08:17:49.984Z","avatar_url":"https://github.com/marko-js.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\n  \u003c!-- Logo --\u003e\n  \u003cbr/\u003e\n  htmljs-parser\n  \u003cbr/\u003e\n\n  \u003c!-- Format --\u003e\n  \u003ca href=\"https://github.com/prettier/prettier\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/styled_with-prettier-ff69b4.svg\" alt=\"Styled with prettier\"/\u003e\n  \u003c/a\u003e\n  \u003c!-- CI --\u003e\n  \u003ca href=\"https://github.com/marko-js/htmljs-parser/actions/workflows/ci.yml\"\u003e\n    \u003cimg src=\"https://github.com/marko-js/htmljs-parser/actions/workflows/ci.yml/badge.svg\" alt=\"Build status\"/\u003e\n  \u003c/a\u003e\n  \u003c!-- Coverage --\u003e\n  \u003ca href=\"https://codecov.io/gh/marko-js/htmljs-parser\"\u003e\n    \u003cimg src=\"https://codecov.io/gh/marko-js/htmljs-parser/branch/main/graph/badge.svg?token=Sv8ePs16ix\" alt=\"Code Coverage\"/\u003e\n  \u003c/a\u003e\n  \u003c!-- NPM Version --\u003e\n  \u003ca href=\"https://npmjs.org/package/htmljs-parser\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/htmljs-parser.svg\" alt=\"NPM version\"/\u003e\n  \u003c/a\u003e\n  \u003c!-- Downloads --\u003e\n  \u003ca href=\"https://npmjs.org/package/htmljs-parser\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/dm/htmljs-parser.svg\" alt=\"Downloads\"/\u003e\n  \u003c/a\u003e\n\u003c/h1\u003e\n\nAn HTML parser with super powers used by [Marko](https://markojs.com/docs/syntax/).\n\n# Installation\n\n```console\nnpm install htmljs-parser\n```\n\n# Creating A Parser\n\nFirst we must create a parser instance and pass it some handlers for the various parse events shown below.\n\nEach parse event is called a `Range` and is an object with start and end properties which are zero-based offsets from the beginning of th parsed code.\n\nAdditional meta data and nested ranges are exposed on some events shown below.\n\nYou can get the raw string from any range using `parser.read(range)`.\n\n```javascript\nimport { createParser, ErrorCode, TagType } from \"htmljs-parser\";\n\nconst parser = createParser({\n  /**\n   * Called when the parser encounters an error.\n   *\n   * @example\n   * 1╭─ \u003ca\u003e\u003cb\n   *  ╰─     ╰─ error(code: 19, message: \"EOF reached while parsing open tag\")\n   */\n  onError(range) {\n    range.code; // An error code id. You can see the list of error codes in ErrorCode imported above.\n    range.message; // A human readable (hopefully) error message.\n  },\n\n  /**\n   * Called when some static text is parsed within some body content.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003eHi\u003c/div\u003e\n   *  ╰─      ╰─ text \"Hi\"\n   */\n  onText(range) {},\n\n  /**\n   * Called after parsing a placeholder within body content.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003e${hello} $!{world}\u003c/div\u003e\n   *  │       │ │      │  ╰─ placeholder.value \"world\"\n   *  │       │ │      ╰─ placeholder \"$!{world}\"\n   *  │       │ ╰─ placeholder:escape.value \"hello\"\n   *  ╰─      ╰─ placeholder:escape \"${hello}\"\n   */\n  onPlaceholder(range) {\n    range.escape; // true for ${} placeholders and false for $!{} placeholders.\n    range.value; // Another range that includes only the placeholder value itself without the wrapping braces.\n  },\n\n  /**\n   * Called when we find a comment at the root of the document or within a tags contents.\n   * It will not be fired for comments within expressions, such as attribute values.\n   *\n   * @example\n   * 1╭─ \u003c!-- hi --\u003e\n   *  │  │   ╰─ comment.value \" hi \"\n   *  ╰─ ╰─ comment \"\u003c!-- hi --\u003e\"\n   * 2╭─ // hi\n   *  │  │ ╰─ comment.value \" hi\"\n   *  ╰─ ╰─ comment \"// hi\"\n   */\n  onComment(range) {\n    range.value; // Another range that only includes the contents of the comment.\n  },\n\n  /**\n   * Called after parsing a CDATA section.\n   * // https://developer.mozilla.org/en-US/docs/Web/API/CDATASection\n   *\n   * @example\n   * 1╭─ \u003c![CDATA[hi]]\u003e\n   *  │  │        ╰─ cdata.value \"hi\"\n   *  ╰─ ╰─ cdata \"\u003c![CDATA[hi]]\u003e\"\n   */\n  onCDATA(range) {\n    range.value; // Another range that only includes the contents of the CDATA.\n  },\n\n  /**\n   * Called after parsing a DocType comment.\n   * https://developer.mozilla.org/en-US/docs/Web/API/DocumentType\n   *\n   * @example\n   * 1╭─ \u003c!DOCTYPE html\u003e\n   *  │  │ ╰─ doctype.value \"DOCTYPE html\"\n   *  ╰─ ╰─ doctype \"\u003c!DOCTYPE html\u003e\"\n   */\n  onDoctype(range) {\n    range.value; // Another range that only includes the contents of the DocType.\n  },\n\n  /**\n   * Called after parsing an XML declaration.\n   * https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction#xml_declaration\n   *\n   * @example\n   * 1╭─ \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n   *  │  │ ╰─ declaration.value \"xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"\"\n   *  ╰─ ╰─ declaration \"\u003c?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?\u003e\"\n   */\n  onDeclaration(range) {\n    range.value; // Another range that only includes the contents of the declaration.\n  },\n\n  /**\n   * Called after parsing a scriptlet (new line followed by a $).\n   *\n   * @example\n   * 1╭─ $ foo();\n   *  │   │╰─ scriptlet.value \"foo();\"\n   *  ╰─  ╰─ scriptlet \" foo();\"\n   * 2╭─ $ { bar(); }\n   *  │   │ ╰─ scriptlet:block.value \" bar(); \"\n   *  ╰─  ╰─ scriptlet:block \" { bar(); }\"\n   */\n  onScriptlet(range) {\n    range.block; // true if the scriptlet was contained within braces.\n    range.value; // Another range that includes only the value itself without the leading $ or surrounding braces (if applicable).\n  },\n\n  /**\n   * Called when we're about to begin an HTML open tag (before the tag name).\n   * Note: This is only called for HTML mode tags and can be used to track if you are in concise mode.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003eHi\u003c/div\u003e\n   *  ╰─ ╰─ openTagStart\n   */\n  onOpenTagStart(range) {},\n\n  /**\n   * Called when a tag name, which can include placeholders, has been parsed.\n   *\n   * @example\n   * 1╭─ \u003cdiv/\u003e\n   *  ╰─  ╰─ openTagName \"div\"\n   * 2╭─ \u003chello-${test}-again/\u003e\n   *  │   │     │      ╰─ openTagName.quasis[1] \"-again\"\n   *  │   │     ╰─ openTagName.expressions[0] \"${test}\"\n   *  │   ├─ openTagName.quasis[0] \"hello-\"\n   *  ╰─  ╰─ openTagName \"hello-${test}-again\"\n   */\n  onOpenTagName(range) {\n    range.quasis; // An array of ranges that indicate the string literal parts of the tag name.\n    range.expressions; // A list of placeholder ranges (similar to whats emitted via onPlaceholder).\n\n    // Return a different tag type enum value to enter a different parse mode.\n    // Below is approximately what Marko uses:\n    switch (parser.read(range)) {\n      case \"area\":\n      case \"base\":\n      case \"br\":\n      case \"col\":\n      case \"embed\":\n      case \"hr\":\n      case \"img\":\n      case \"input\":\n      case \"link\":\n      case \"meta\":\n      case \"param\":\n      case \"source\":\n      case \"track\":\n      case \"wbr\":\n        // TagType.void makes this a void element (cannot have children).\n        return TagType.void;\n      case \"html-comment\":\n      case \"script\":\n      case \"style\":\n      case \"textarea\":\n        // TagType.text makes the child content text only (with placeholders).\n        return TagType.text;\n      case \"class\":\n      case \"export\":\n      case \"import\":\n      case \"static\":\n        // TagType.statement makes this a statement tag where the content following the tag name will be parsed as script code until we reach a new line, eg for `import x from \"y\"`).\n        return TagType.statement;\n    }\n\n    // TagType.html is the default which allows child content as html with placeholders.\n    return TagType.html;\n  },\n\n  /**\n   * Called when a shorthand id, which can include placeholders, has been parsed.\n   *\n   * @example\n   * 1╭─ \u003cdiv#hello-${test}-again/\u003e\n   *  │      ││     │       ╰─ tagShorthandId.quasis[1] \"-again\"\n   *  │      ││     ╰─ tagShorthandId.expressions[0] \"${test}\"\n   *  │      │╰─ tagShorthandId.quasis[0] \"hello-\"\n   *  ╰─     ╰─ tagShorthandId \"#hello-${test}-again\"\n   */\n  onTagShorthandId(range) {\n    range.quasis; // An array of ranges that indicate the string literal parts of the shorthand id name.\n    range.expressions; // A list of placeholder ranges (similar to whats emitted via onPlaceholder).\n  },\n\n  /**\n   * Called when a shorthand class name, which can include placeholders, has been parsed.\n   * Note there can be multiple of these.\n   *\n   * @example\n   * 1╭─ \u003cdiv.hello-${test}-again/\u003e\n   *  │      ││     │       ╰─ tagShorthandClassName.quasis[1] \"-again\"\n   *  │      ││     ╰─ tagShorthandClassName.expressions[0] \"${test}\"\n   *  │      │╰─ tagShorthandClassName.quasis[0] \"hello-\"\n   *  ╰─     ╰─ tagShorthandClassName \"#hello-${test}-again\"\n   */\n  onTagShorthandClass(range) {\n    range.quasis; // An array of ranges that indicate the string literal parts of the shorthand id name.\n    range.expressions; // A list of placeholder ranges (similar to whats emitted via onPlaceholder).\n  },\n\n  /**\n   * Called after the type arguments for a tag have been parsed.\n   *\n   * @example\n   * 1╭─ \u003cfoo\u003cstring\u003e\u003e\n   *  │      │╰─ tagTypeArgs.value \"string\"\n   *  ╰─     ╰─ tagTypeArgs \"\u003cstring\u003e\"\n   */\n  onTagTypeArgs(range) {\n    range.value; // Another range that includes only the type arguments themselves and not the angle brackets.\n  },\n\n  /**\n   * Called after a tag variable has been parsed.\n   *\n   * @example\n   * 1╭─ \u003cdiv/el/\u003e\n   *  │      │╰─ tagVar.value \"el\"\n   *  ╰─     ╰─ tagVar \"/el\"\n   */\n  onTagVar(range) {\n    range.value; // Another range that includes only the tag var itself and not the leading slash.\n  },\n\n  /**\n   * Called after tag arguments have been parsed.\n   *\n   * @example\n   * 1╭─ \u003cif(x)\u003e\n   *  │     │╰─ tagArgs.value \"x\"\n   *  ╰─    ╰─ tagArgs \"(x)\"\n   */\n  onTagArgs(range) {\n    range.value; // Another range that includes only the args themselves and not the outer parenthesis.\n  },\n\n  /**\n   * Called after type parameters for the tag parameters have been parsed.\n   *\n   * @example\n   * 1╭─ \u003ctag\u003cT\u003e|input: { name: T }|\u003e\n   *  │      │╰─ tagTypeParams.value\n   *  ╰─     ╰─ tagTypeParams \"\u003cT\u003e\"\n   */\n  onTagTypeParams(range) {\n    range.value; // Another range that includes only the type params themselves and not the angle brackets.\n  },\n\n  /**\n   * Called after tag parameters have been parsed.\n   *\n   * @example\n   * 1╭─ \u003cfor|item| of=list\u003e\n   *  │      │╰─ tagParams.value \"item\"\n   *  ╰─     ╰─ tagParams \"|item|\"\n   */\n  onTagParams(range) {\n    range.value; // Another range that includes only the params themselves and not the outer pipes.\n  },\n\n  /**\n   * Called after an attribute name as been parsed.\n   * Note this may be followed by the related AttrArgs, AttrValue or AttrMethod. It can also be directly followed by another AttrName, AttrSpread or the OpenTagEnd if this is a boolean attribute.\n   *\n   * @example\n   * 1╭─ \u003cdiv class=\"hi\"\u003e\n   *  ╰─      ╰─ attrName \"class\"\n   */\n  onAttrName(range) {},\n\n  /**\n   * Called after attr arguments have been parsed.\n   *\n   * @example\n   * 1╭─ \u003cdiv if(x)\u003e\n   *  │         │╰─ attrArgs.value \"x\"\n   *  ╰─        ╰─ attrArgs \"(x)\"\n   */\n  onAttrArgs(range) {\n    range.value; // Another range that includes only the args themselves and not the outer parenthesis.\n  },\n\n  /**\n   * Called after an attr value has been parsed.\n   *\n   * @example\n   * 1╭─ \u003cinput name=\"hi\" value:=x\u003e\n   *  │             ││         │ ╰─ attrValue:bound.value\n   *  │             ││         ╰─ attrValue:bound \":=x\"\n   *  │             │╰─ attrValue.value \"\\\"hi\\\"\"\n   *  ╰─            ╰─ attrValue \"=\\\"hi\\\"\"\n   */\n  onAttrValue(range) {\n    range.bound; // true if the attribute value was preceded by :=.\n    range.value; // Another range that includes only the value itself without the leading = or :=.\n  },\n\n  /**\n   * Called after an attribute method shorthand has been parsed.\n   *\n   * @example\n   * 1╭─ \u003cdiv onClick(ev) { foo(); }\u003e\n   *  │              ││   │╰─ attrMethod.body.value \" foo(); \"\n   *  │              ││   ╰─ attrMethod.body \"{ foo(); }\"\n   *  │              │╰─ attrMethod.params.value \"ev\"\n   *  │              ├─ attrMethod.params \"(ev)\"\n   *  ╰─             ╰─ attrMethod \"(ev) { foo(); }\"\n   */\n  onAttrMethod(range) {\n    range.typeParams; // Another range which includes the type params for the method.\n    range.typeParams.value; // Another range which includes the type params without outer angle brackets.\n\n    range.params; // Another range which includes the params for the method.\n    range.params.value; // Another range which includes the method params without outer parenthesis.\n\n    range.body; // Another range which includes the entire body block.\n    range.body.value; // Another range which includes the body block without outer braces.\n  },\n\n  /**\n   * Called after we've parsed a spread attribute.\n   *\n   * @example\n   * 1╭─ \u003cdiv ...attrs\u003e\n   *  │       │  ╰─ attrSpread.value \"attrs\"\n   *  ╰─      ╰─ attrSpread \"...attrs\"\n   */\n  onAttrSpread(range) {\n    range.value; // Another range that includes only the value itself without the leading ...\n  },\n\n  /**\n   * Called once we've completed parsing the open tag.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003e\u003cspan/\u003e\u003c/div\u003e\n   *  │      │     ╰─ openTagEnd:selfClosed \"/\u003e\"\n   *  ╰─     ╰─ openTagEnd \"\u003e\"\n   */\n  onOpenTagEnd(range) {\n    range.selfClosed; // true if this tag was self closed (the onCloseTag* handlers will not be called if so).\n  },\n\n  /**\n   * Called when we start parsing and html closing tag.\n   * Note this is not emitted for concise, selfClosed, void or statement tags.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003e\u003cspan/\u003e\u003c/div\u003e\n   *  ╰─             ╰─ closeTagStart \"\u003c/\"\n   */\n  onCloseTagStart(range) {},\n\n  /**\n   * Called after the content within the brackets of an html closing tag has been parsed.\n   * Note this is not emitted for concise, selfClosed, void or statement tags.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003e\u003cspan/\u003e\u003c/div\u003e\n   *  ╰─               ╰─ closeTagName \"div\"\n   */\n  onCloseTagName(range) {},\n\n  /**\n   * Called once the closing tag has finished parsing, or in concise mode we hit an outdent or eof.\n   * Note this is not called for selfClosed, void or statement tags.\n   *\n   * @example\n   * 1╭─ \u003cdiv\u003e\u003cspan/\u003e\u003c/div\u003e\n   *  ╰─                  ╰─ closeTagEnd \"\u003e\"\n   */\n  onCloseTagEnd(range) {},\n});\n```\n\nFinally after setting up the parser with it's handlers, it's time to pass in some source code to parse.\n\n```javascript\nparser.parse(\"\u003cdiv\u003e\u003c/div\u003e\");\n```\n\n# Parser Helpers\n\nThe parser instance provides a few helpers to make it easier to work with the parsed content.\n\n```javascript\n// Pass any range object into this method to get the raw string from the source for the range.\nparser.read(range);\n\n// Given an zero based offset within the source code, returns a position object that contains line and column properties.\nparser.positionAt(offset);\n\n// Given a range object returns a location object with start and end properties which are each position objects as returned from the \"positionAt\" api.\nparser.locationAt(range);\n```\n\n# Code of Conduct\n\nThis project adheres to the [eBay Code of Conduct](./.github/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarko-js%2Fhtmljs-parser","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarko-js%2Fhtmljs-parser","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarko-js%2Fhtmljs-parser/lists"}