├── .c8rc.json ├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── parsing-bug-report.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── build_and_test.yaml │ ├── dependabot.yml │ ├── publish.yaml │ └── release-drafter.yml ├── .gitignore ├── .husky └── pre-commit ├── .mocharc.json ├── .nvmrc ├── .prettierignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── demo.css ├── demo.js └── index.tpl.html ├── package.json ├── rollup.config.js ├── src ├── backcompat │ ├── adr.ts │ ├── geo.ts │ ├── hentry.ts │ ├── hfeed.ts │ ├── hnews.ts │ ├── hproduct.ts │ ├── hresume.ts │ ├── hreview-aggregate.ts │ ├── hreview.ts │ ├── index.ts │ ├── item.ts │ ├── vcard.ts │ └── vevent.ts ├── helpers │ ├── array.ts │ ├── attributes.ts │ ├── documentSetup.ts │ ├── experimental.ts │ ├── findChildren.ts │ ├── images.ts │ ├── includes.ts │ ├── metaformats.ts │ ├── nodeMatchers.ts │ ├── textContent.ts │ ├── url.ts │ └── valueClassPattern.ts ├── implied │ ├── name.ts │ ├── photo.ts │ └── url.ts ├── index.ts ├── microformats │ ├── parse.ts │ ├── properties.ts │ └── property.ts ├── parser.ts ├── rels │ └── rels.ts ├── types.ts └── validator.ts ├── test ├── package.cjs.spec.js ├── package.mjs.spec.js ├── scenarios.spec.ts ├── suites │ ├── README.md │ ├── experimental │ │ ├── lang-feed.html │ │ ├── lang-feed.json │ │ ├── lang-meta.html │ │ ├── lang-meta.json │ │ ├── lang.html │ │ ├── lang.json │ │ ├── metaformats-missing-head.html │ │ ├── metaformats-missing-head.json │ │ ├── metaformats-og-article.html │ │ ├── metaformats-og-article.json │ │ ├── metaformats-og-audio-soundcloud.html │ │ ├── metaformats-og-audio-soundcloud.json │ │ ├── metaformats-og-profile-linkedin.html │ │ ├── metaformats-og-profile-linkedin.json │ │ ├── metaformats-og-video-vimeo.html │ │ ├── metaformats-og-video-vimeo.json │ │ ├── metaformats-prefer-mf.html │ │ ├── metaformats-prefer-mf.json │ │ ├── metaformats-standard.html │ │ ├── metaformats-standard.json │ │ ├── metaformats-twitter-article.html │ │ ├── metaformats-twitter-article.json │ │ ├── text-content.html │ │ └── text-content.json │ └── local │ │ ├── microformats-v1 │ │ ├── includes.html │ │ └── includes.json │ │ └── microformats-v2 │ │ ├── dates.html │ │ ├── dates.json │ │ ├── empty-property.html │ │ ├── empty-property.json │ │ ├── lang.html │ │ ├── lang.json │ │ ├── nested.html │ │ ├── nested.json │ │ ├── rel-urls.html │ │ ├── rel-urls.json │ │ ├── urls.html │ │ └── urls.json ├── utils │ ├── dirname.d.ts │ ├── dirname.js │ ├── loadScenarios.d.ts │ └── loadScenarios.js └── validation.spec.ts ├── tsconfig.json └── yarn.lock /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": true, 4 | "include": ["src/**"], 5 | "exclude": ["src/types.ts"], 6 | "statements": 99, 7 | "branches": 98.5, 8 | "functions": 100, 9 | "lines": 99 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:import/errors", 6 | "plugin:import/typescript", 7 | "prettier" 8 | ], 9 | "ignorePatterns": ["node_modules/", "dist/", "public/", "**/*.html"], 10 | "settings": { 11 | "import/resolver": { 12 | "node": { "extensions": [".ts"] } 13 | } 14 | }, 15 | "rules": { 16 | "arrow-body-style": ["error", "as-needed"], 17 | "import/order": [ 18 | "error", 19 | { 20 | "groups": [["builtin", "external", "internal"]], 21 | "newlines-between": "always-and-inside-groups" 22 | } 23 | ] 24 | }, 25 | "overrides": [ 26 | { 27 | "files": ["./demo/**/*.js", "./rollup.config.js"], 28 | "rules": { 29 | "@typescript-eslint/no-var-requires": "off", 30 | "@typescript-eslint/explicit-function-return-type": "off", 31 | "@typescript-eslint/explicit-module-boundary-types": "off" 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for new microformats support 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **What type of feature is it?** 10 | 11 | Is it an experimental feature or a new addition to the specification? 12 | 13 | **Describe the feature** 14 | 15 | Please provide a couple of sentences describing what will change with this feature. 16 | 17 | **Example of input** 18 | 19 | Provide clear examples of input HTML that covers the proposed feature. 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | **Example of output** 26 | 27 | Please provide the expected JSON output for the provided HTML. 28 | 29 | ```json 30 | 31 | ``` 32 | 33 | **Additional context** 34 | 35 | Add any other context or information about the feature request here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/parsing-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Parsing bug report 3 | about: Create a bug report for when microformats are incorrectly parsed 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. Please include a reference to the specification, other discussions or other parser behaviour. 12 | 13 | **To Reproduce** 14 | 15 | HTML input: 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | **Expected behavior** 22 | 23 | Correct JSON output: 24 | 25 | ```json 26 | 27 | ``` 28 | 29 | **Additional context** 30 | 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | **Checklist** 4 | 5 | 6 | 7 | - [ ] Added validaton to any changes in the parser API. 8 | - [ ] Added tests covering the parsing behaviour changes. 9 | - [ ] Linked to any relevant issues this will close. 10 | - [ ] Tested the output using the [demo](../CONTRIBUTING.md#testing-your-changes). 11 | 12 | **Changes to parsing behaviour** 13 | 14 | 15 | 16 | A brief summary of any changes to the parser behaviour. 17 | 18 | **Example input covered by new behaviour** 19 | 20 | 21 | 22 | ```html 23 | 24 | ``` 25 | 26 | **Example output from new behaviour** 27 | 28 | 29 | 30 | ```json 31 | 32 | ``` 33 | 34 | **Other changes** 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: npm 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | versioning-strategy: increase-if-necessary 13 | commit-message: 14 | prefix: "chore(deps): " 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | commit-message: 20 | prefix: "chore(deps): " 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | template: | 4 | ## What's Changed 5 | 6 | $CHANGES 7 | categories: 8 | - title: Breaking changes 9 | labels: 10 | - major 11 | - breaking change 12 | - title: Features 13 | labels: 14 | - enhancement 15 | - minor 16 | - title: Bug fixes 17 | labels: 18 | - bug 19 | - title: Maintenance 20 | labels: 21 | - maintenance 22 | - documentation 23 | version-resolver: 24 | major: 25 | labels: 26 | - major 27 | - breaking change 28 | minor: 29 | labels: 30 | - minor 31 | - enhancement 32 | patch: 33 | labels: 34 | - bug 35 | - maintenance 36 | default: patch 37 | autolabeler: 38 | - label: bug 39 | title: 40 | - "/fix/i" 41 | - label: maintenance 42 | title: 43 | - "/perf/i" 44 | - "/refactor/i" 45 | - "/style/i" 46 | - "/test/i" 47 | - "/build/i" 48 | - "/chore/i" 49 | - "/ci/i" 50 | - label: enhancement 51 | title: 52 | - "/feat/i" 53 | - label: breaking change 54 | title: 55 | - "/breaking change/i" 56 | - label: documentation 57 | title: 58 | - "/docs/i" 59 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | TZ: Europe/London 11 | 12 | jobs: 13 | build_and_test: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version-file: ".nvmrc" 21 | cache: "npm" 22 | - name: Install dependencies 23 | run: yarn 24 | - name: Lint code 25 | run: yarn lint 26 | - name: Run prettier list 27 | run: yarn prettier:list 28 | - name: Test code 29 | run: yarn test 30 | - name: Build package 31 | run: yarn build 32 | - name: Upload build artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: dist 36 | path: dist 37 | test_dist: 38 | runs-on: ubuntu-latest 39 | timeout-minutes: 5 40 | needs: [build_and_test] 41 | strategy: 42 | matrix: 43 | node: [18, 20, 22, 24] 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-node@v4 47 | with: 48 | node-version: ${{ matrix.node }} 49 | - name: Install dependencies 50 | run: yarn 51 | - name: Download build artifacts 52 | uses: actions/download-artifact@v4 53 | with: 54 | name: dist 55 | path: dist 56 | - name: Test package 57 | run: yarn test:package 58 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-approve 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Approve PR 19 | run: gh pr review --approve "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | - name: Auto-merge PR 24 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 25 | run: | 26 | gh pr review --approve "$PR_URL" | 27 | gh pr merge --auto --squash "$PR_URL" 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish NPM package 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version-file: ".nvmrc" 16 | registry-url: https://registry.npmjs.org/ 17 | - name: Install dependencies 18 | run: yarn 19 | - name: Set correct version 20 | run: npm --no-git-tag-version version $VERSION 21 | env: 22 | VERSION: ${{ github.event.release.tag_name }} 23 | - name: Build package 24 | run: yarn build 25 | - name: Publish to NPM 26 | run: yarn publish --non-interactive 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | - name: Deploy pages 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./public 34 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release drafter 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | types: [opened, reopened, synchronize] 7 | 8 | jobs: 9 | update_release_draft: 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: release-drafter/release-drafter@v6 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | public 107 | demo/dist 108 | 109 | .DS_Store 110 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts", "mjs", "cjs", "js"], 3 | "node-option": [ 4 | "experimental-specifier-resolution=node", 5 | "loader=ts-node/esm" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .nyc_output/ 3 | .cache 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

Contributing

2 | 3 | - [Ways to contribute](#ways-to-contribute) 4 | - [Making Changes](#making-changes) 5 | - [I don't know TypeScript or my tests won't pass](#i-dont-know-typescript-or-my-tests-wont-pass) 6 | - [Testing your changes](#testing-your-changes) 7 | - [Microformats specifications](#microformats-specifications) 8 | - [Node support](#node-support) 9 | - [Developer environment](#developer-environment) 10 | - [Node/yarn version](#nodeyarn-version) 11 | - [Developer tools](#developer-tools) 12 | - [License](#license) 13 | 14 | ## Ways to contribute 15 | 16 | Anyone can contribute to this project in one of many ways, for example: 17 | 18 | - Create an issue for a bug 19 | - Open a pull request to make an improvement 20 | - Open a pull request to fix a bug 21 | - Participate in discussion 22 | 23 | ## Making Changes 24 | 25 | 1. Fork the repo into your own GitHub account and clone it to your local machine. 26 | 2. Use the versions of node and yarn from [development environment](#developer-environment). 27 | 3. Create a branch for the code changes you're looking to make with `git checkout -b branch-name`. 28 | 4. Add some tests to our test suites to describe the change you want to make. These are in the form of JSON/HTML pairs. 29 | 5. Write some code to pass the tests! 30 | 6. Commit your changes using `git commit -am 'A description of the change'`. We try to follow [conventional commit types](https://github.com/commitizen/conventional-commit-types), but this is not required. 31 | 7. Push the branch to your fork: `git push -u origin branch-name`. 32 | 8. Create a new pull request! 33 | 34 | ### I don't know TypeScript or my tests won't pass 35 | 36 | You don't need to make your Pull Request perfect! The important thing is to get a PR open so we can begin making this parser better. 37 | 38 | We're more than happy to help with any TypeScript, linting or test problems, or to refactor after a merge. These should not be a barrier to contributing! 39 | 40 | ### Testing your changes 41 | 42 | You can test your changes using the interactive demo. Just run `yarn build` and `yarn demo` and visit `http://localhost:8080` to parse a real-world example. 43 | 44 | ## Microformats specifications 45 | 46 | This project follows the [microformats2 parsing specification](http://microformats.org/wiki/microformats2-parsing) and tests all code against the [microformats test suite](https://github.com/microformats/tests). 47 | 48 | All pull requests making changes to the parsing behaviour should reference the relevant specification and provide additional tests to cover the change. 49 | 50 | ## Node support 51 | 52 | We support all versions that are currently supported on the [node LTS schedule](https://nodejs.org/en/about/releases/). 53 | 54 | ## Developer environment 55 | 56 | ### Node/yarn version 57 | 58 | This project is developed using: 59 | 60 | - node@lts (active) 61 | - yarn@1 62 | 63 | If you use `nvm`, you can run `nvm use` in the root of this project to switch to the correct version of node. 64 | 65 | ### Developer tools 66 | 67 | We use a few developer tools to help maintain code quality. 68 | 69 | - TypeScript is used to statically typecheck all code. 70 | - Prettier (`yarn prettier:list`) is used as an opinionated code-formatter. A fix command is executed on each commit automatically. 71 | - ESLint (`yarn lint`) validates your code against specific rules. A check is executed on each commit automatically, and will prevent a commit if there are any errors found. 72 | - Mocha (`yarn test`) tests the package against a set of tests (located in `/test`). These are ran automatically in CI for each push. 73 | - These tests are ran against the [microformats test suite](https://github.com/microformats/tests) and some additional test cases. 74 | - Mocha tests for the built package is ran against all supported LTS versions of node. 75 | - Tests require 100% code coverage to pass. 76 | - Tests are not required to pass to be able to commit. 77 | - More information on adding new tests is available in the [test suite README](./test/suites/README.md). 78 | 79 | ## License 80 | 81 | By contributing to this project, you agree that any contributions are made under the [MIT license](https://choosealicense.com/licenses/mit/). 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aimee Gamble-Milner 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microformats-parser 2 | 3 | A JavaScript microformats v2 parser, with v1 back-compatibility. [View the demo](https://microformats.github.io/microformats-parser/). Works with both the browser and node.js. 4 | 5 | Follows the [microformats2 parsing specification](http://microformats.org/wiki/microformats2-parsing). 6 | 7 | **Table of contents** 8 | 9 | - [Quick start](#quick-start) 10 | - [Installation](#installation) 11 | - [Simple use](#simple-use) 12 | - [API](#api) 13 | - [mf2()](#mf2) 14 | - [Support](#support) 15 | - [Microformats v1](#microformats-v1) 16 | - [Microformats v2](#microformats-v2) 17 | - [Experimental options](#experimental-options) 18 | - [`lang`](#lang) 19 | - [`textContent`](#textcontent) 20 | - [`metaformats`](#metaformats) 21 | - [Contributing](#contributing) 22 | 23 | ## Quick start 24 | 25 | ### Installation 26 | 27 | ```bash 28 | # yarn 29 | yarn add microformats-parser 30 | 31 | # npm 32 | npm i microformats-parser 33 | ``` 34 | 35 | ### Simple use 36 | 37 | ```javascript 38 | const { mf2 } = require("microformats-parser"); 39 | 40 | const parsed = mf2('Jimmy', { 41 | baseUrl: "http://example.com/", 42 | }); 43 | 44 | console.log(parsed); 45 | ``` 46 | 47 | Outputs: 48 | 49 | ```json 50 | { 51 | "items": [ 52 | { 53 | "properties": { 54 | "name": ["Jimmy"], 55 | "url": ["http://example.com/"] 56 | }, 57 | "type": ["h-card"] 58 | } 59 | ], 60 | "rel-urls": { 61 | "http://example.com": { 62 | "rels": ["me"], 63 | "text": "Jimmy" 64 | } 65 | }, 66 | "rels": { 67 | "me": ["http://example.com/"] 68 | } 69 | } 70 | ``` 71 | 72 | ## API 73 | 74 | ### mf2() 75 | 76 | Use: `mf2(html: string, options: { baseUrl: string, experimental: object })` 77 | 78 | - `html` (string, required) - the HTML string to be parsed 79 | - `options` (object, required) - parsing options, with the following properties: 80 | - `baseUrl` (string, required) - a base URL to resolve relative URLs 81 | - `experimental` (object, optional) - experimental (non-standard) options 82 | - `lang` (boolean, optional) - enable support for parsing `lang` attributes 83 | - `textContent` (boolean, optional) - enable support for better collapsing whitespace in text content. 84 | - `metaformats` (boolean, optional) - enable meta tag fallback. 85 | 86 | Returns the parsed microformats from the HTML string 87 | 88 | ## Support 89 | 90 | ### Microformats v1 91 | 92 | This package will parse microformats v1, however support will be limited to the v1 tests in the [microformats test suite](https://github.com/microformats/tests). Contributions are still welcome for improving v1 support. 93 | 94 | ### Microformats v2 95 | 96 | We provide support for all microformats v2 parsing, as detailed in the [microformats2 parsing specification](http://microformats.org/wiki/microformats2-parsing). If there is an issue with v2 parsing, please create an issue. 97 | 98 | ### Experimental options 99 | 100 | There is also support for some experimental parsing options. These can be enabled with the `experimental` flags in the `options` API. 101 | 102 | **Note: Experimental options are subject to change at short notice and may change their behaviour without a major version update** 103 | 104 | #### `lang` 105 | 106 | Parse microformats for `lang` attributes. This will include `lang` on microformats and `e-*` properties where available. 107 | 108 | These are sourced from the element themselves, a parent microformat, the HTML document or a meta tag. 109 | 110 | #### `textContent` 111 | 112 | When parsing microformats for text content, all the consecutive whitespace is collapsed into a single space. `
` and `

` tags are treated as line breaks. 113 | 114 | #### `metaformats` 115 | 116 | Enables fallback to [metaformats](https://microformats.org/wiki/metaformats) parsing which looks at `` tags to infer content. 117 | 118 | ## Contributing 119 | 120 | See our [contributing guidelines](./CONTRIBUTING.md) for more information. 121 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | h1, 8 | p, 9 | ul, 10 | li, 11 | body, 12 | html { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | html, 18 | body { 19 | font-size: 16px; 20 | font-family: "Source Sans Pro", sans-serif; 21 | } 22 | 23 | a { 24 | color: #177e89; 25 | text-decoration: none; 26 | } 27 | 28 | a:hover { 29 | color: #2b3238; 30 | } 31 | 32 | nav { 33 | background: #2b3238; 34 | } 35 | 36 | nav ul { 37 | margin: 0 -1rem; 38 | } 39 | 40 | nav ul li { 41 | display: inline-block; 42 | padding: 0.5rem; 43 | } 44 | 45 | nav a { 46 | color: #fff; 47 | } 48 | 49 | nav a:hover { 50 | color: #177e89; 51 | } 52 | 53 | header { 54 | background: #e9ecef; 55 | text-align: center; 56 | padding: 2rem 1rem; 57 | } 58 | 59 | header h1 { 60 | margin-bottom: 2rem; 61 | } 62 | 63 | footer { 64 | margin-top: 4rem; 65 | margin-bottom: 2rem; 66 | text-align: center; 67 | } 68 | 69 | .description { 70 | margin-bottom: 2rem; 71 | } 72 | 73 | .container { 74 | max-width: 750px; 75 | margin: 0 auto; 76 | padding: 0 1rem; 77 | } 78 | 79 | .documentation, 80 | button[type="submit"] { 81 | background: #08605f; 82 | color: #fff; 83 | display: inline-block; 84 | border-radius: 0.25rem; 85 | border: none; 86 | padding: 0.5rem 1rem; 87 | } 88 | 89 | .submit { 90 | text-align: center; 91 | } 92 | 93 | .documentation { 94 | padding: 0.75rem 1.5rem; 95 | font-size: 1.25rem; 96 | } 97 | 98 | .documentation:hover, 99 | button[type="submit"]:hover { 100 | background: #2b3238; 101 | color: #fff; 102 | cursor: pointer; 103 | } 104 | 105 | #result { 106 | border-radius: 0.25rem; 107 | border: 1px solid #ccc; 108 | background: #f4f4f4; 109 | min-height: 10rem; 110 | overflow: scroll; 111 | padding: 0.5rem; 112 | font-size: 0.8rem; 113 | } 114 | 115 | form label { 116 | display: block; 117 | padding: 1rem 0; 118 | } 119 | 120 | form input[type="text"], 121 | form textarea { 122 | width: 100%; 123 | display: block; 124 | border: 1px solid #177e89; 125 | background: #fff; 126 | border-radius: 0.25rem; 127 | padding: 0.5rem; 128 | } 129 | 130 | form textarea { 131 | min-height: 10rem; 132 | min-width: 100%; 133 | max-width: 100%; 134 | } 135 | 136 | .error { 137 | border: 1px solid #df3b57; 138 | border-radius: 0.25rem; 139 | padding: 0.5rem 1rem; 140 | color: #df3b57; 141 | margin: 2rem 0; 142 | } 143 | 144 | .hide { 145 | display: none; 146 | } 147 | 148 | .experimental label { 149 | display: inline-block; 150 | } 151 | 152 | h3 { 153 | font-size: 1rem; 154 | margin: 0; 155 | } 156 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { mf2 } from "../dist/index.mjs"; 3 | import "./demo.css"; 4 | 5 | const setResult = (result) => { 6 | const escaped = JSON.stringify(result, null, 2) 7 | .replace(//g, ">"); 9 | document.getElementById("result").innerHTML = escaped; 10 | }; 11 | 12 | const setError = (error) => { 13 | const el = document.getElementById("error"); 14 | el.innerHTML = `Error: ${error}`; 15 | el.classList.remove("hide"); 16 | }; 17 | 18 | const parse = (html, options) => { 19 | document.getElementById("error").classList.add("hide"); 20 | 21 | try { 22 | const result = mf2(html, options); 23 | setResult(result); 24 | } catch (err) { 25 | setError(err.message); 26 | } 27 | 28 | return false; 29 | }; 30 | 31 | window.parseHtml = () => { 32 | const html = document.getElementById("html").value; 33 | const baseUrl = document.getElementById("base-url").value; 34 | const lang = document.getElementById("lang").checked; 35 | const textContent = document.getElementById("textContent").checked; 36 | const metaformats = document.getElementById("metaformats").checked; 37 | 38 | return parse(html, { 39 | baseUrl, 40 | experimental: { lang, textContent, metaformats }, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /demo/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 9 | {{ links }} 10 | 11 | 12 | 13 | {{ metas }} 14 | 15 | 16 |

31 |
32 |
33 |

{{ name }}

34 |

{{ description }}

35 |

36 | Documentation 37 |

38 |
39 |
40 |
41 |

Try it out

42 | 49 |
50 | 54 | 55 | 59 | 60 |

Experimental options

61 |

62 | 66 | 76 | 86 |

87 | 88 |
89 | 90 |
91 |
92 |
93 |
94 |

Output

95 |

 96 |       
97 |
98 | 103 | {{ scripts }} 104 | 105 | 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microformats-parser", 3 | "version": "2.0.1", 4 | "description": "A JavaScript microformats v2 parser for the browser and node.js", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "types": "dist/index.d.ts", 8 | "module": "dist/index.mjs", 9 | "homepage": "https://microformats.github.io/microformats-parser/", 10 | "repository": "https://github.com/microformats/microformats-parser.git", 11 | "author": "Aimee Gamble-Milner (https://github.com/aimee-gm)", 12 | "license": "MIT", 13 | "keywords": [ 14 | "microformats", 15 | "parser", 16 | "mf2", 17 | "indieweb" 18 | ], 19 | "scripts": { 20 | "prepare": "husky", 21 | "prebuild": "rm -rf ./dist", 22 | "build": "rollup -c", 23 | "demo": "http-server public", 24 | "lint": "eslint --cache './**/*.{ts,js}'", 25 | "prettier:list": "prettier '**/*.{ts,json,md,html}' --list-different", 26 | "prettier:fix": "prettier '**/*.{ts,json,md,html}' --write", 27 | "test": "c8 mocha ./test/*.spec.ts", 28 | "test:package": "mocha ./test/package.*.spec.js" 29 | }, 30 | "engines": { 31 | "node": ">=18" 32 | }, 33 | "files": [ 34 | "/dist/index.cjs", 35 | "/dist/index.cjs.map", 36 | "/dist/index.mjs", 37 | "/dist/index.mjs.map", 38 | "/dist/index.d.ts", 39 | "/CONTRIBUTING.md" 40 | ], 41 | "dependencies": { 42 | "parse5": "^7.1.2" 43 | }, 44 | "devDependencies": { 45 | "@rollup/plugin-commonjs": "^25.0.4", 46 | "@rollup/plugin-html": "^1.0.2", 47 | "@rollup/plugin-node-resolve": "^15.2.1", 48 | "@rollup/plugin-terser": "^0.4.3", 49 | "@rollup/plugin-typescript": "^11.1.3", 50 | "@types/chai": "^4.2.11", 51 | "@types/glob": "^8.1.0", 52 | "@types/mocha": "^10.0.1", 53 | "@types/node": "^20.8.6", 54 | "@typescript-eslint/eslint-plugin": "^7.0.0", 55 | "@typescript-eslint/parser": "^6.0.0", 56 | "c8": "^9.0.0", 57 | "chai": "^5.0.0", 58 | "eslint": "^8.16.0", 59 | "eslint-config-prettier": "^9.0.0", 60 | "eslint-plugin-import": "^2.26.0", 61 | "glob": "^10.3.4", 62 | "http-server": "^14.1.1", 63 | "husky": ">=4", 64 | "lint-staged": ">=10", 65 | "microformat-tests": "https://github.com/microformats/tests", 66 | "mocha": "^11.0.0", 67 | "prettier": "^3.0.3", 68 | "rollup": "^4.1.5", 69 | "rollup-plugin-dts": "^6.0.2", 70 | "rollup-plugin-import-css": "^3.3.3", 71 | "source-map-support": "^0.5.19", 72 | "ts-node": "^10.8.0", 73 | "tslib": "^2.6.2", 74 | "typescript": "^5.2.2" 75 | }, 76 | "lint-staged": { 77 | "*.{js,ts,json,css,md,html}": "prettier --write", 78 | "*.{js,ts}": "eslint --fix" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import nodeResolve from "@rollup/plugin-node-resolve"; 4 | import terser from "@rollup/plugin-terser"; 5 | import { dts } from "rollup-plugin-dts"; 6 | import html, { makeHtmlAttributes } from "@rollup/plugin-html"; 7 | import { readFileSync } from "fs"; 8 | import css from "rollup-plugin-import-css"; 9 | 10 | import pkg from "./package.json" assert { type: "json" }; 11 | const { name, description, version, repository, license, keywords } = pkg; 12 | 13 | export default [ 14 | { 15 | input: "./src/index.ts", 16 | external: (id) => 17 | !id.startsWith("\0") && !id.startsWith(".") && !id.startsWith("/"), 18 | plugins: [ 19 | typescript({ 20 | outputToFilesystem: true, 21 | tsconfig: "./tsconfig.json", 22 | }), 23 | nodeResolve(), 24 | commonjs({ 25 | ignoreGlobal: true, 26 | }), 27 | terser({ 28 | compress: { 29 | passes: 2, 30 | }, 31 | }), 32 | ], 33 | output: [ 34 | { 35 | exports: "named", 36 | format: "cjs", 37 | sourcemap: true, 38 | file: "dist/index.cjs", 39 | }, 40 | { 41 | format: "esm", 42 | sourcemap: true, 43 | file: "dist/index.mjs", 44 | }, 45 | ], 46 | }, 47 | { 48 | input: "./dist/types/index.d.ts", 49 | output: [{ file: "dist/index.d.ts", format: "es" }], 50 | plugins: [dts()], 51 | }, 52 | { 53 | input: "./demo/demo.js", 54 | output: { 55 | dir: "./public", 56 | format: "esm", 57 | entryFileNames: "assets/bundle-[hash].js", 58 | }, 59 | plugins: [ 60 | nodeResolve(), 61 | commonjs({ 62 | ignoreGlobal: true, 63 | }), 64 | css(), 65 | html({ 66 | title: `${name} demo`, 67 | template: ({ attributes, files, meta, publicPath, title }) => { 68 | const repo = repository.replace("git+", "").replace(".git", ""); 69 | const scripts = (files.js || []) 70 | .map(({ fileName }) => { 71 | const attrs = makeHtmlAttributes(attributes.script); 72 | return ``; 73 | }) 74 | .join("\n"); 75 | 76 | const links = (files.css || []) 77 | .map(({ fileName }) => { 78 | const attrs = makeHtmlAttributes(attributes.link); 79 | return ``; 80 | }) 81 | .join("\n"); 82 | 83 | const metas = meta 84 | .map((input) => { 85 | const attrs = makeHtmlAttributes(input); 86 | return ``; 87 | }) 88 | .join("\n"); 89 | 90 | const replacements = { 91 | scripts, 92 | links, 93 | title, 94 | name, 95 | description, 96 | metas, 97 | version: `v${version}`, 98 | repo, 99 | releases: `${repo}/releases`, 100 | licenseUrl: `${repo}/blob/main/LICENSE`, 101 | npm: `https://npmjs.org/package/${name}`, 102 | license, 103 | keywords: keywords.join(", "), 104 | }; 105 | 106 | let template = readFileSync("./demo/index.tpl.html", "utf-8"); 107 | Object.entries(replacements).forEach(([key, value]) => { 108 | template = template.replaceAll(`{{ ${key} }}`, value); 109 | }); 110 | return template; 111 | }, 112 | }), 113 | ], 114 | }, 115 | ]; 116 | -------------------------------------------------------------------------------- /src/backcompat/adr.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const adr: Backcompat = { 4 | type: ["h-adr"], 5 | properties: { 6 | "country-name": "p-country-name", 7 | locality: "p-locality", 8 | region: "p-region", 9 | "street-address": "p-street-address", 10 | "postal-code": "p-postal-code", 11 | "extended-address": "p-extended-address", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/backcompat/geo.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const geo: Backcompat = { 4 | type: ["h-geo"], 5 | properties: { 6 | latitude: "p-latitude", 7 | longitude: "p-longitude", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/backcompat/hentry.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hentry: Backcompat = { 4 | type: ["h-entry"], 5 | properties: { 6 | author: "p-author", 7 | "entry-content": "e-content", 8 | "entry-summary": "p-summary", 9 | "entry-title": "p-name", 10 | updated: "dt-updated", 11 | }, 12 | rels: { 13 | bookmark: "u-url", 14 | tag: "p-category", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/backcompat/hfeed.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hfeed: Backcompat = { 4 | type: ["h-feed"], 5 | properties: { 6 | author: "p-author", 7 | photo: "u-photo", 8 | url: "u-url", 9 | }, 10 | rels: { 11 | tag: "p-category", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/backcompat/hnews.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hnews: Backcompat = { 4 | type: ["h-news"], 5 | properties: { 6 | entry: "p-entry", 7 | "source-org": "p-source-org", 8 | dateline: "p-dateline", 9 | geo: "p-geo", 10 | }, 11 | rels: { 12 | principles: "u-principles", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/backcompat/hproduct.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hproduct: Backcompat = { 4 | type: ["h-product"], 5 | properties: { 6 | price: "p-price", 7 | description: "p-description", 8 | fn: "p-name", 9 | review: "p-review", 10 | brand: "p-brand", 11 | url: "u-url", 12 | photo: "u-photo", 13 | }, 14 | rels: { 15 | tag: "p-category", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/backcompat/hresume.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hresume: Backcompat = { 4 | type: ["h-resume"], 5 | properties: { 6 | contact: "p-contact", 7 | experience: "p-experience", 8 | summary: "p-summary", 9 | skill: "p-skill", 10 | education: "p-education", 11 | affiliation: "p-affiliation", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/backcompat/hreview-aggregate.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hreviewAggregate: Backcompat = { 4 | type: ["h-review-aggregate"], 5 | properties: { 6 | rating: "p-rating", 7 | average: "p-average", 8 | best: "p-best", 9 | count: "p-count", 10 | item: "p-item", 11 | url: "u-url", 12 | fn: "p-name", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/backcompat/hreview.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const hreview: Backcompat = { 4 | type: ["h-review"], 5 | properties: { 6 | item: "p-item", 7 | rating: "p-rating", 8 | reviewer: "p-author", 9 | summary: "p-name", 10 | url: "u-url", 11 | description: "e-content", 12 | }, 13 | rels: { 14 | bookmark: "u-url", 15 | tag: "p-category", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/backcompat/index.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "../types"; 2 | import { adr } from "./adr"; 3 | import { geo } from "./geo"; 4 | import { hentry } from "./hentry"; 5 | import { hfeed } from "./hfeed"; 6 | import { hnews } from "./hnews"; 7 | import { hproduct } from "./hproduct"; 8 | import { hreview } from "./hreview"; 9 | import { vcard } from "./vcard"; 10 | import { 11 | getClassNameIntersect, 12 | hasClassNameIntersect, 13 | getRelIntersect, 14 | hasRelIntersect, 15 | getAttributeValue, 16 | getClassNames, 17 | } from "../helpers/attributes"; 18 | import { hreviewAggregate } from "./hreview-aggregate"; 19 | import { hresume } from "./hresume"; 20 | import { vevent } from "./vevent"; 21 | import { item } from "./item"; 22 | import { flatten } from "../helpers/array"; 23 | 24 | export const backcompat = { 25 | adr, 26 | geo, 27 | hentry, 28 | hfeed, 29 | hnews, 30 | hproduct, 31 | hreview, 32 | vcard, 33 | hresume, 34 | vevent, 35 | item, 36 | "hreview-aggregate": hreviewAggregate, 37 | }; 38 | 39 | export type BackcompatRoot = keyof typeof backcompat; 40 | 41 | export const backcompatRoots = Object.keys(backcompat) as BackcompatRoot[]; 42 | 43 | export const getBackcompatRootClassNames = (node: Element): BackcompatRoot[] => 44 | getClassNameIntersect(node, backcompatRoots); 45 | 46 | export const convertV1RootClassNames = (node: Element): string[] => { 47 | const classNames = getBackcompatRootClassNames(node) 48 | .map((cl) => backcompat[cl].type) 49 | .reduce(flatten); 50 | 51 | return classNames.length > 1 52 | ? classNames.filter((cl) => cl !== "h-item") 53 | : classNames; 54 | }; 55 | 56 | export const hasBackcompatMicroformatProperty = ( 57 | node: Element, 58 | roots: BackcompatRoot[], 59 | ): boolean => 60 | roots.some((root) => { 61 | const { properties, rels } = backcompat[root]; 62 | return ( 63 | hasClassNameIntersect(node, Object.keys(properties)) || 64 | (rels && hasRelIntersect(node, Object.keys(rels))) 65 | ); 66 | }); 67 | 68 | export const convertV1PropertyClassNames = ( 69 | node: Element, 70 | roots: BackcompatRoot[], 71 | ): string[] => [ 72 | ...new Set( 73 | roots 74 | .map((root) => { 75 | const { properties, rels } = backcompat[root]; 76 | 77 | const classes = getClassNameIntersect( 78 | node, 79 | Object.keys(properties), 80 | ).map((cl) => properties[cl]); 81 | 82 | const relClasses = 83 | (rels && 84 | getRelIntersect(node, Object.keys(rels)).map((cl) => rels[cl])) || 85 | []; 86 | 87 | return [...classes, ...relClasses]; 88 | }) 89 | .reduce(flatten), 90 | ), 91 | ]; 92 | 93 | export const getV1IncludeNames = (node: Element): string[] => { 94 | const itemref = getAttributeValue(node, "itemref"); 95 | 96 | if (itemref) { 97 | return itemref.split(" "); 98 | } 99 | 100 | if (getClassNames(node).includes("include")) { 101 | const hrefAttr = node.tagName === "object" ? "data" : "href"; 102 | 103 | const href = getAttributeValue(node, hrefAttr); 104 | 105 | if (href && href.startsWith("#")) { 106 | return [href.substring(1)]; 107 | } 108 | } 109 | 110 | const headers = node.tagName === "td" && getAttributeValue(node, "headers"); 111 | 112 | if (headers) { 113 | return [headers]; 114 | } 115 | 116 | return []; 117 | }; 118 | -------------------------------------------------------------------------------- /src/backcompat/item.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const item: Backcompat = { 4 | type: ["h-item"], 5 | properties: { 6 | fn: "p-name", 7 | photo: "u-photo", 8 | url: "u-url", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/backcompat/vcard.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const vcard: Backcompat = { 4 | type: ["h-card"], 5 | properties: { 6 | fn: "p-name", 7 | url: "u-url", 8 | org: "p-org", 9 | adr: "p-adr", 10 | tel: "p-tel", 11 | title: "p-job-title", 12 | email: "u-email", 13 | photo: "u-photo", 14 | agent: "p-agent", 15 | "family-name": "p-family-name", 16 | "given-name": "p-given-name", 17 | "additional-name": "p-additional-name", 18 | "honorific-prefix": "p-honorific-prefix", 19 | "honorific-suffix": "p-honorific-suffix", 20 | key: "p-key", 21 | label: "p-label", 22 | logo: "u-logo", 23 | mailer: "p-mailer", 24 | nickname: "p-nickname", 25 | note: "p-note", 26 | sound: "u-sound", 27 | geo: "p-geo", 28 | bday: "dt-bday", 29 | class: "p-class", 30 | rev: "p-rev", 31 | role: "p-role", 32 | "sort-string": "p-sort-string", 33 | tz: "p-tz", 34 | uid: "u-uid", 35 | }, 36 | rels: { 37 | tag: "p-category", 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/backcompat/vevent.ts: -------------------------------------------------------------------------------- 1 | import { Backcompat } from "../types"; 2 | 3 | export const vevent: Backcompat = { 4 | type: ["h-event"], 5 | properties: { 6 | summary: "p-name", 7 | dtstart: "dt-start", 8 | dtend: "dt-end", 9 | duration: "dt-duration", 10 | description: "p-description", 11 | attendee: "p-attendee", 12 | location: "p-location", 13 | url: "u-url", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/helpers/array.ts: -------------------------------------------------------------------------------- 1 | export const flatten = (prev: T[], curr: T[]): T[] => [...prev, ...curr]; 2 | -------------------------------------------------------------------------------- /src/helpers/attributes.ts: -------------------------------------------------------------------------------- 1 | import { Attribute, Element } from "../types"; 2 | 3 | export const getAttribute = ( 4 | node: Element, 5 | name: string, 6 | ): Attribute | undefined => node.attrs.find((attr) => attr.name === name); 7 | 8 | export const getAttributeValue = ( 9 | node: Element, 10 | name: string, 11 | ): string | undefined => getAttribute(node, name)?.value; 12 | 13 | export const getClassNames = ( 14 | node: Element, 15 | matcher?: RegExp | string, 16 | ): string[] => { 17 | const classNames = getAttributeValue(node, "class")?.split(" ") || []; 18 | 19 | return matcher 20 | ? classNames.filter((name) => 21 | typeof matcher === "string" 22 | ? name.startsWith(matcher) 23 | : name.match(matcher), 24 | ) 25 | : classNames; 26 | }; 27 | 28 | export const getClassNameIntersect = ( 29 | node: Element, 30 | toCompare: T[], 31 | ): T[] => 32 | getClassNames(node).filter((name: string): name is T => 33 | toCompare.includes(name as T), 34 | ); 35 | 36 | export const hasClassName = (node: Element, className: string): boolean => 37 | getClassNames(node).some((name) => name === className); 38 | 39 | export const hasClassNameIntersect = ( 40 | node: Element, 41 | toCompare: string[], 42 | ): boolean => getClassNames(node).some((name) => toCompare.includes(name)); 43 | 44 | export const getAttributeIfTag = ( 45 | node: Element, 46 | tagNames: string[], 47 | attr: string, 48 | ): string | undefined => 49 | tagNames.includes(node.tagName) ? getAttributeValue(node, attr) : undefined; 50 | 51 | export const hasRelIntersect = (node: Element, toCompare: string[]): boolean => 52 | Boolean( 53 | getAttributeValue(node, "rel") 54 | ?.split(" ") 55 | .some((name) => toCompare.includes(name)), 56 | ); 57 | 58 | export const getRelIntersect = (node: Element, toCompare: string[]): string[] => 59 | getAttributeValue(node, "rel") 60 | ?.split(" ") 61 | .filter((name) => toCompare.includes(name)) || []; 62 | -------------------------------------------------------------------------------- /src/helpers/documentSetup.ts: -------------------------------------------------------------------------------- 1 | import { ParserOptions, IdRefs, Rels, RelUrls } from "../types"; 2 | import { getAttribute, getAttributeValue } from "./attributes"; 3 | import { isLocalLink, applyBaseUrl } from "./url"; 4 | import { isElement, isRel, isBase } from "./nodeMatchers"; 5 | import { parseRel } from "../rels/rels"; 6 | import { Document, Element } from "../types"; 7 | 8 | interface DocumentSetupResult { 9 | idRefs: IdRefs; 10 | rels: Rels; 11 | relUrls: RelUrls; 12 | baseUrl: string; 13 | lang?: string; 14 | } 15 | 16 | export const findBase = (node: Element | Document): string | undefined => { 17 | for (const child of node.childNodes) { 18 | if (!isElement(child)) { 19 | continue; 20 | } 21 | 22 | if (isBase(child)) { 23 | return getAttributeValue(child, "href"); 24 | } 25 | 26 | const base = findBase(child); 27 | 28 | if (base) { 29 | return base; 30 | } 31 | } 32 | 33 | return; 34 | }; 35 | 36 | // this is mutating the object, and will mutate it for everything else :-/ 37 | 38 | const handleNode = ( 39 | node: Element | Document, 40 | result: DocumentSetupResult, 41 | options: ParserOptions, 42 | ): void => { 43 | for (const i in node.childNodes) { 44 | const child = node.childNodes[i]; 45 | 46 | if (!isElement(child)) { 47 | continue; 48 | } 49 | 50 | /** 51 | * Delete