{"id":13691960,"url":"https://github.com/kostya/modest","last_synced_at":"2026-02-14T03:18:56.602Z","repository":{"id":140663271,"uuid":"74370290","full_name":"kostya/modest","owner":"kostya","description":"CSS selectors for HTML5 Parser myhtml","archived":false,"fork":false,"pushed_at":"2018-08-04T18:21:12.000Z","size":179,"stargazers_count":46,"open_issues_count":2,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-07-17T16:38:17.305Z","etag":null,"topics":["crystal","css","css-selector","selectors"],"latest_commit_sha":null,"homepage":"","language":"Crystal","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/kostya.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2016-11-21T14:13:15.000Z","updated_at":"2024-03-09T22:14:18.000Z","dependencies_parsed_at":"2023-04-15T14:34:17.334Z","dependency_job_id":null,"html_url":"https://github.com/kostya/modest","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/kostya/modest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kostya%2Fmodest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kostya%2Fmodest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kostya%2Fmodest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kostya%2Fmodest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kostya","download_url":"https://codeload.github.com/kostya/modest/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kostya%2Fmodest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29433304,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T02:20:56.896Z","status":"ssl_error","status_checked_at":"2026-02-14T02:11:29.478Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["crystal","css","css-selector","selectors"],"created_at":"2024-08-02T17:00:52.030Z","updated_at":"2026-02-14T03:18:56.573Z","avatar_url":"https://github.com/kostya.png","language":"Crystal","funding_links":[],"categories":["HTML/XML Parsing"],"sub_categories":[],"readme":"## WARNING, this shard obsolete and moved to [myhtml](https://github.com/kostya/myhtml) directly, use [myhtml](https://github.com/kostya/myhtml) \u003e= 1.0.0\n\n# modest\n\nCSS selectors for HTML5 Parser [myhtml](https://github.com/kostya/myhtml) (Crystal wrapper for https://github.com/lexborisov/Modest).\n\n## Installation\n\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  modest:\n    github: kostya/modest\n```\n\n## Usage of CSS Selectors with myhtml parser\n\n```crystal\nrequire \"modest\"\n\npage = \u003c\u003c-PAGE\n  \u003chtml\u003e\n    \u003cdiv class=aaa\u003e\u003cp id=bbb\u003e\u003ca href=\"http://...\" class=ccc\u003ebla\u003c/a\u003e\u003c/div\u003e\n  \u003c/html\u003e\nPAGE\n\nmyhtml = Myhtml::Parser.new(page)\n\n# css select from the root! scope (equal with myhtml.root!.css(\"...\"))\niterator = myhtml.css(\"div.aaa p#bbb a.ccc\") # =\u003e Iterator(Myhtml::Node), methods: .each, .to_a, ...\n\niterator.each do |node|\n  p node.tag_id              # MyHTML_TAG_A\n  p node.tag_name            # \"a\"\n  p node.tag_sym             # :a\n  p node.attributes[\"href\"]? # \"http://...\"\n  p node.inner_text          # \"bla\"\n  puts node.to_html          # \u003ca href=\"http://...\" class=\"ccc\"\u003ebla\u003c/a\u003e\nend\n\n# css select from node scope\nif p_node = myhtml.css(\"div.aaa p#bbb\").first?\n  p_node.css(\"a.ccc\").each do |node|\n    p node.tag_sym # :a\n  end\nend\n\n```\n\n## Example 2\n\n```crystal\nrequire \"modest\"\n\nhtml = \u003c\u003c-PAGE\n  \u003cdiv\u003e\n    \u003cp id=p1\u003e\n    \u003cp id=p2 class=jo\u003e\n    \u003cp id=p3\u003e\n      \u003ca href=\"some.html\" id=a1\u003elink1\u003c/a\u003e\n      \u003ca href=\"some.png\" id=a2\u003elink2\u003c/a\u003e\n    \u003cdiv id=bla\u003e\n      \u003cp id=p4 class=jo\u003e\n      \u003cp id=p5 class=bu\u003e\n      \u003cp id=p6 class=jo\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\nPAGE\n\nparser = Myhtml::Parser.new(html)\n\n# select all p nodes which id like `*p*`\np parser.css(\"p[id*=p]\").map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p1\", \"p2\", \"p3\", \"p4\", \"p5\", \"p6\"]\n\n# select all nodes with class \"jo\"\np parser.css(\"p.jo\").map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p2\", \"p4\", \"p6\"]\np parser.css(\".jo\").map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p2\", \"p4\", \"p6\"]\n\n# select odd child tag inside div, which not contain a\np parser.css(\"div \u003e :nth-child(2n+1):not(:has(a))\").map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p1\", \"p4\", \"p6\"]\n\n# all elements with class=jo inside last div tag\np parser.css(\"div\").to_a.last.css(\".jo\").map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p4\", \"p6\"]\n\n# a element with href ends like .png\np parser.css(%q{a[href$=\".png\"]}).map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"a2\"]\n\n# find all a tags inside \u003cp id=p3\u003e, which href contain `html`\np parser.css(%q{p[id=p3] \u003e a[href*=\"html\"]}).map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"a1\"]\n\n# find all a tags inside \u003cp id=p3\u003e, which href contain `html` or ends_with `.png`\np parser.css(%q{p[id=p3] \u003e a:matches([href *= \"html\"], [href $= \".png\"])}).map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"a1\", \"a2\"]\n\n# create finder and use it in many places, this is faster, than create it many times\nfinder = Modest::Finder.new(\".jo\")\np parser.css(finder).map(\u0026.attribute_by(\"id\")).to_a # =\u003e [\"p2\", \"p4\", \"p6\"]\n```\n\n## Example 3\n```crystal\nrequire \"modest\"\n\nhtml = \u003c\u003c-PAGE\n  \u003chtml\u003e\u003cbody\u003e\n  \u003ctable id=\"t1\"\u003e\u003ctbody\u003e\n  \u003ctr\u003e\u003ctd\u003eHello\u003c/td\u003e\u003c/tr\u003e\n  \u003c/tbody\u003e\u003c/table\u003e\n  \u003ctable id=\"t2\"\u003e\u003ctbody\u003e\n  \u003ctr\u003e\u003ctd\u003e123\u003c/td\u003e\u003ctd\u003eother\u003c/td\u003e\u003c/tr\u003e\n  \u003ctr\u003e\u003ctd\u003efoo\u003c/td\u003e\u003ctd\u003ecolumns\u003c/td\u003e\u003c/tr\u003e\n  \u003ctr\u003e\u003ctd\u003ebar\u003c/td\u003e\u003ctd\u003eare\u003c/td\u003e\u003c/tr\u003e\n  \u003ctr\u003e\u003ctd\u003exyz\u003c/td\u003e\u003ctd\u003eignored\u003c/td\u003e\u003c/tr\u003e\n  \u003c/tbody\u003e\u003c/table\u003e\n  \u003c/body\u003e\u003c/html\u003e\nPAGE\n\nparser = Myhtml::Parser.new(html)\n\np parser.css(\"#t2 tr td:first-child\").map(\u0026.inner_text).to_a # =\u003e [\"123\", \"foo\", \"bar\", \"xyz\"]\np parser.css(\"#t2 tr td:first-child\").map(\u0026.to_html).to_a # =\u003e [\"\u003ctd\u003e123\u003c/td\u003e\", \"\u003ctd\u003efoo\u003c/td\u003e\", \"\u003ctd\u003ebar\u003c/td\u003e\", \"\u003ctd\u003exyz\u003c/td\u003e\"]\n```\n\n## Benchmark\n\nComparing with nokorigi(libxml), and crystagiri(libxml). Parse 1000 times google page, code: https://github.com/kostya/modest/tree/master/bench\n\n```crystal\nrequire \"modest\"\npage = File.read(\"./google.html\")\ns = 0\nlinks = [] of String\n1000.times do\n  myhtml = Myhtml::Parser.new(page)\n  links = myhtml.css(\"div.g h3.r a\").map(\u0026.attribute_by(\"href\")).to_a\n  s += links.size\n  myhtml.free\nend\np links.last\np s\n```\n\nParse + Selectors\n\n| Lang     |  Package           | Time, s | Memory, MiB |\n| -------- | ------------------ | ------- | ----------- |\n| Crystal  | modest(myhtml)     | 2.52    | 7.7         |\n| Crystal  | Crystagiri(LibXML) | 19.89   | 14.3        |\n| Ruby 2.2 | Nokogiri(LibXML)   | 45.05   | 136.2       |\n\nSelectors Only (files with suffix 2)\n\n| Lang     |  Package           | Time, s | Memory, MiB |\n| -------- | ------------------ | ------- | ----------- |\n| Crystal  | modest(myhtml)     | 0.18    | 4.6         |\n| Crystal  | Crystagiri(LibXML) | 12.30   | 6.6         |\n| Ruby 2.2 | Nokogiri(LibXML)   | 28.06   | 68.8        |\n\n\n## CSS Selectors rules\nhttps://drafts.csswg.org/selectors-4/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkostya%2Fmodest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkostya%2Fmodest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkostya%2Fmodest/lists"}