From 555a2c41c242515f92ff11ab07649e9395d26223 Mon Sep 17 00:00:00 2001 From: Esteban Borai Date: Thu, 29 Jul 2021 20:38:19 -0400 Subject: [PATCH] init: first commit --- .github/PULL_REQUEST_TEMPLATE.md | 27 + .github/dependabot.yml | 9 + .github/workflows/build.yml | 29 + .github/workflows/clippy.yml | 37 + .github/workflows/fmt.yml | 39 + .github/workflows/publish-dry-run.yml | 31 + .github/workflows/release.yml | 43 + .github/workflows/test.yml | 29 + .gitignore | 55 + CHANGELOG.md | 13 + Cargo.lock | 1401 +++++++++++++++++++++++++ Cargo.toml | 24 + LICENSE-APACHE | 201 ++++ LICENSE-MIT | 25 + README.md | 25 + html/full_featured.html | 73 ++ html/og_compilant.html | 65 ++ html/schema_compilant.html | 58 + html/twitter_compilant.html | 61 ++ rustfmt.toml | 278 +++++ src/html.rs | 39 + src/lib.rs | 15 + src/og.rs | 168 +++ src/preview.rs | 291 +++++ src/schema.rs | 66 ++ src/twitter.rs | 102 ++ 26 files changed, 3204 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/fmt.yml create mode 100644 .github/workflows/publish-dry-run.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 html/full_featured.html create mode 100644 html/og_compilant.html create mode 100644 html/schema_compilant.html create mode 100644 html/twitter_compilant.html create mode 100644 rustfmt.toml create mode 100644 src/html.rs create mode 100644 src/lib.rs create mode 100644 src/og.rs create mode 100644 src/preview.rs create mode 100644 src/schema.rs create mode 100644 src/twitter.rs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..717f67c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1224165 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: 'cargo' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..747df06 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: build +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Cache .cargo and target + uses: actions/cache@v2 + with: + path: | + ~/.cargo + ./target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo-build + - name: cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --locked diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..984b4f8 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,37 @@ +name: clippy +on: + pull_request: + push: + branches: + - main + +jobs: + clippy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + + - name: Cache .cargo and target + uses: actions/cache@v2 + with: + path: | + ~/.cargo + ./target + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo-clippy + + - name: cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 0000000..65a6649 --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,39 @@ +name: fmt +on: + pull_request: + push: + branches: + - main + +jobs: + fmt: + name: fmt + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + + - name: Cache .cargo and target + uses: actions/cache@v2 + with: + path: | + ~/.cargo + ./target + key: ${{ runner.os }}-cargo-fmt-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-fmt-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo-fmt + + - name: Run fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/.github/workflows/publish-dry-run.yml b/.github/workflows/publish-dry-run.yml new file mode 100644 index 0000000..b069162 --- /dev/null +++ b/.github/workflows/publish-dry-run.yml @@ -0,0 +1,31 @@ +name: publish-dry-run +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Cache .cargo and target + uses: actions/cache@v2 + with: + path: | + ~/.cargo + ./target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo-build + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + + - name: Dry Rung Cargo Publish + run: cargo publish --dry-run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fadeee0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: release + +on: + push: + tags: + - 'v*' + +jobs: + create-release: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: '' + draft: false + prerelease: false + + publish-crate: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - run: cargo login ${CRATES_IO_TOKEN} + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + + - name: publish crate + run: cargo publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ed7e21c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: test +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Cache .cargo and target + uses: actions/cache@v2 + with: + path: | + ~/.cargo + ./target + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo-test + + - name: cargo test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da1a46d --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Development # +############### +.env + +# Application Related # +####################### +/target +/tmp + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +bundle + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + + +# Cargo # +######### +/target + +# Editor Files # +################ +/.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3b10a0f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.1] - 2021-07-31 +### Added +- `LinkPreview` struct implementation +- Introduces `fetch` feature to fetch sites using `reqwest` +- Basic support for Open Graph tags scrapping +- Basic support for Twitter Cards tags scrapping +- Basic support for Shema.org tags scrapping diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3132b1b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1401 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "html5ever" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "hyper" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "link-preview" +version = "0.0.1" +dependencies = [ + "reqwest", + "scraper", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scraper" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e02aa790c80c2e494130dec6a522033b6a23603ffc06360e9fe6c611ea2c12" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "matches", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" + +[[package]] +name = "serde_json" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c5e91e4240b46c4c19219d6cc84784444326131a4210f496f948d5cc827a29" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "siphasher" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1" + +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "socket2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "pin-project-lite", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a87a235 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "link-preview" +version = "0.0.1" +authors = ["Esteban Borai "] +edition = "2018" +description = "Retrieve website metadata such as title, description, preview image, author and more from OpenGraph, Google, Schema.org and Twitter compliant sites" +keywords = ["link", "schema-org", "open-graph", "website", "metadata"] +license = "MIT OR Apache-2.0" +readme = "README.md" +categories = ["web-programming"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11.4", optional = true } +scraper = "0.12.0" +thiserror = "1.0.26" +url = "2.2.2" + +[dev-dependencies] +tokio = { version = "1.9.0", features = ["rt", "macros"] } + +[features] +fetch = ["reqwest"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..588025c --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Esteban Borai and Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..6cf02d3 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2021 Esteban Borai and Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7008f06 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +
+

link-preview

+

+ 🦀 Retrieve website metadata such as title, description, preview image, author and more from OpenGraph, Google, Schema.org and Twitter compliant sites +

+
+ +
+ + [![Crates.io](https://img.shields.io/crates/v/link-preview.svg)](https://crates.io/crates/link-preview) + [![Documentation](https://docs.rs/link-preview/badge.svg)](https://docs.rs/link-preview) + ![Build](https://github.com/EstebanBorai/link-preview/workflows/build/badge.svg) + ![Clippy](https://github.com/EstebanBorai/link-preview/workflows/clippy/badge.svg) + ![Formatter](https://github.com/EstebanBorai/link-preview/workflows/fmt/badge.svg) + ![Tests](https://github.com/EstebanBorai/link-preview/workflows/test/badge.svg) + +
+ +> This solution is under development, the API is subject to change. + +## Testing Websites for Rich Results + +Google provides the [Rich Results Analysis Tool](https://search.google.com/test/rich-results?utm_source=support.google.com/webmasters/&utm_medium=referral&utm_campaign=7445569) to help you validate your website's tags. + +Use this tool to make sure the website follows these conventions. diff --git a/html/full_featured.html b/html/full_featured.html new file mode 100644 index 0000000..82d26c8 --- /dev/null +++ b/html/full_featured.html @@ -0,0 +1,73 @@ + + + + + + + + Meta Tags Pro + + + + + + + + + + + + + + + + + + + + + +

+ Search engine optimization + ( + SEO +

+ +
+ ) is the process of improving the quality and quantity of + website traffic + to a + website + or a + web page + from + search engines + . + + [1] + + SEO targets unpaid traffic (known as "natural" or " + organic + " results) rather than direct traffic or + paid traffic + . Unpaid traffic may originate from different kinds of searches, including + image search + , + video search + , + academic search + , + + [2] + + news search, and industry-specific + vertical search + engines. +

+ + + diff --git a/html/og_compilant.html b/html/og_compilant.html new file mode 100644 index 0000000..d60bb61 --- /dev/null +++ b/html/og_compilant.html @@ -0,0 +1,65 @@ + + + + + + + + Open Graph's Tags Testing HTML + + + + + + + + + + + + + + +

+ Search engine optimization + ( + SEO +

+ +
+ ) is the process of improving the quality and quantity of + website traffic + to a + website + or a + web page + from + search engines + . + + [1] + + SEO targets unpaid traffic (known as "natural" or " + organic + " results) rather than direct traffic or + paid traffic + . Unpaid traffic may originate from different kinds of searches, including + image search + , + video search + , + academic search + , + + [2] + + news search, and industry-specific + vertical search + engines. +

+ + + \ No newline at end of file diff --git a/html/schema_compilant.html b/html/schema_compilant.html new file mode 100644 index 0000000..d326e23 --- /dev/null +++ b/html/schema_compilant.html @@ -0,0 +1,58 @@ + + + + + + + + Schema.org Tags Testing HTML + + + + + + + + + +

+ Search engine optimization + ( + SEO +

+ +
+ ) is the process of improving the quality and quantity of + website traffic + to a + website + or a + web page + from + search engines + . + + [1] + + SEO targets unpaid traffic (known as "natural" or " + organic + " results) rather than direct traffic or + paid traffic + . Unpaid traffic may originate from different kinds of searches, including + image search + , + video search + , + academic search + , + + [2] + + news search, and industry-specific + vertical search + engines. +

+ + + diff --git a/html/twitter_compilant.html b/html/twitter_compilant.html new file mode 100644 index 0000000..df1b0b8 --- /dev/null +++ b/html/twitter_compilant.html @@ -0,0 +1,61 @@ + + + + + + + + Twitter Tags Testing HTML + + + + + + + + + + + +

+ Search engine optimization + ( + SEO +

+ +
+ ) is the process of improving the quality and quantity of + website traffic + to a + website + or a + web page + from + search engines + . + + [1] + + SEO targets unpaid traffic (known as "natural" or " + organic + " results) rather than direct traffic or + paid traffic + . Unpaid traffic may originate from different kinds of searches, including + image search + , + video search + , + academic search + , + + [2] + + news search, and industry-specific + vertical search + engines. +

+ + + diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d4d60d2 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,278 @@ +# Use verbose output. +# Default: false +# verbose = + +# Do not reformat out of line modules. +# Default: false +# skip_children = + +# Lines to format; this is not supported in rustfmt.toml, +# and can only be specified via the --file-lines option. +# file_lines = + +# Maximum width of each line. +# Default: 100 +# max_width = + +# Ideal width of each line. +# Default: 80 +# max_width = + +# Number of spaces per tab. +# Default: 4 +# tab_spaces = 4 + +# Maximum width of the args of a function call before +# falling back to vertical formatting. +# Default: 60 +# fn_call_width = + +# Maximum width in the body of a struct lit before falling back to vertical formatting. +# Default: 16 +# struct_lit_width = + +# Maximum width in the body of a struct variant before falling back to vertical formatting. +# Default: 35 +# struct_variant_width = + +# Always print the abi for extern items. +# Default: true +# force_explicit_abi = + +# Unix or Windows line endings. +# Values: Windows | Unix | Native +# Default: Unix +# newline_style = + +# Brace style for functions. +# Values: AlwaysNextLine | PreferSameLine | SameLineWhere +# Default: SameLineWhere +# fn_brace_style = + +# Brace style for structs and enums. +# Values: AlwaysNextLine | PreferSameLine | SameLineWhere +# Default: SameLineWhere +# item_brace_style = + +# Brace style for control flow construct. +# Values: AlwaysSameLine | ClosingNextLine | AlwaysNextLine +# Default: AlwaysSameLine +# control_brace_style = + +# Put empty-body implementations on a single line. +# Default: true +# impl_empty_single_line = + +# Put empty-body functions on a single line. +# Default: true +# fn fn_empty_single_line = + +# Put single-expression functions on a single line. +# Default: false +# fn_single_line = + +# Location of return type in function declaration. +# Values: WithArgs | WithWhereClause +# Default: WithArgs +# fn_return_indent = + +# If function argument parenthesis goes on a newline. +# Default: true +# fn_args_paren_newline = + +# Argument density in functions. +# Values: Compressed | Tall | CompressedIfEmpty | Vertical +# Default: Tall +# fn_args_density = + +# Layout of function arguments. +# Values: Visual | Block | BlockAlways +# Default: Visual +# fn_args_layout = + +# Indent on function arguments. +# Values: Inherit | Tabbed | Visual +# Default: Visual +# fn_arg_indent = + +# Determines if '+' or '=' are wrapped in spaces in the punctuation of types. +# Values: Compressed | Wide +# Default: Wide +# type_punctuation_density = + +# Density of a where clause. +# Values: Compressed | Tall | CompressedIfEmpty | Vertical +# Default: CompressedIfEmpty +# where_density = + +# Indentation of a where clause. +# Values: Inherit | Tabbed | Visual +# Default: Tabbed +# where_indent = + +# Element layout inside a where clause. +# Values: Vertical | Horizontal | HorizontalVertical | Mixed +# Default: Vertical +# where_layout = + +# Indentation style of a where predicate. +# Values: Inherit | Tabbed | Visual +# Default: Visual +# where_pred_indent = + +# Put a trailing comma on where clauses. +# Default: false +# where_trailing_comma = + +# Indentation of generics. +# Values: Inherit | Tabbed | Visual +# Default: Visual +# generics_indent = + +# If there is a trailing comma on structs. +# Values: Always | Never | Vertical +# Default: Vertical +# struct_trailing_comma = + +# If there is a trailing comma on literal structs. +# Values: Always | Never | Vertical +# Default: Vertical +# struct_lit_trailing_comma = + +# Style of struct definition. +# Values: Visual | Block +# Default: Block +# struct_lit_style = + +# Multiline style on literal structs. +# Values: PreferSingle | ForceMulti +# Default: PreferSingle +# struct_lit_multiline_style = + +# Put a trailing comma on enum declarations. +# Default: true +# enum_trailing_comma = + +# Report all, none or unnumbered occurrences of TODO in source file comments. +# Values: Always | Unnumbered | Never +# Default: Never +# report_todo = + +# Report all, none or unnumbered occurrences of FIXME in source file comments. +# Values: Always | Unnumbered | Never +# Default: Never +# report_fixme = + +# Indent on chain base. +# Values: Inherit | Tabbed | Visual +# Default: Tabbed +# chain_base_indent = + +# Indentation of chain. +# Values: Inherit | Tabbed | Visual +# Default: Tabbed +# chain_indent = + +# Allow last call in method chain to break the line. +# Default: true +# chains_overflow_last = + +# Reorder import statements alphabetically. +# Default: false +# reorder_imports = + +# Reorder lists of names in import statements alphabetically. +# Default: false +# reorder_imported_names = + +# Maximum line length for single line if-else expressions. +# A value of zero means always break if-else expressions. +# Default: 50 +# single_line_if_else_max_width = + +# Format string literals where necessary. +# Default: true +# format_strings = + +# Always format string literals. +# Default: false +# force_format_strings = + +# Retain some formatting characteristics from the source code. +# Default: true +# take_source_hints = + +# Use tab characters for indentation, spaces for alignment. +# Default: false +# hard_tabs = + +# Break comments to fit on the line. +# Default: false +# wrap_comments = + +# Convert /* */ comments to // comments where possible. +# Default: false +# normalize_comments = + +# Wrap multiline match arms in blocks. +# Default: true +# wrap_match_arms = + +# Put a trailing comma after a block based match arm (non-block arms are not affected). +# Default: false +# match_block_trailing_comma = + +# Put a trailing comma after a wildcard arm. +# Default: true +# match_wildcard_trailing_comma = + +# How many lines a closure must have before it is block indented. +# -1 means never use block indent. +# Type: +# Default: 5 +# closure_block_indent_threshold = + +# Leave a space before the colon in a type annotation. +# Default: false +# space_before_type_annotation = + +# Leave a space after the colon in a type annotation. +# Default: true +# space_after_type_annotation_colon = + +# Leave a space before the colon in a trait or lifetime bound. +# Default: false +# space_before_bound = + +# Leave a space after the colon in a trait or lifetime bound. +# Default: true +# space_after_bound_colon = + +# Put spaces around the .. and ... range operators. +# Default: false +# spaces_around_ranges = + +# Put spaces within non-empty generic arguments. +# Default: false +# spaces_within_angle_brackets = + +# Put spaces within non-empty square brackets. +# Default: false +# spaces_within_square_brackets = + +# Put spaces within non-empty parentheses. +# Default: false +# spaces_within_parens = + +# Replace uses of the try! macro by the ? shorthand. +# Default: false +# use_try_shorthand = + +# What Write Mode to use when none is supplied: Replace, Overwrite, Display, Diff, Coverage. +# Values: Replace | Overwrite | Display | Diff | Coverage | Plain | Checkstyle +# Default: Replace +# write_mode = + +# Replace strings of _ wildcards by a single .. in tuple patterns. +# Default: false +# condense_wildcard_suffices = diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..e49a030 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,39 @@ +use scraper::{Html, Selector}; + +pub fn first_inner_html(html: &Html, tag: &str) -> Option { + let selector = Selector::parse(tag).unwrap(); + + if let Some(element) = html.select(&selector).next() { + let value = element.inner_html(); + + if !value.is_empty() { + return Some(value); + } + } + + None +} + +pub fn find_meta_tag(html: &Html, property: &str) -> Option { + let selector = Selector::parse(&format!("meta[property=\"{}\"]", property)).unwrap(); + + if let Some(element) = html.select(&selector).next() { + if let Some(value) = element.value().attr("content") { + return Some(value.to_string()); + } + } + + None +} + +pub fn find_link(html: &Html, rel: &str) -> Option { + let selector = Selector::parse(&format!("link[rel=\"{}\"]", rel)).unwrap(); + + if let Some(element) = html.select(&selector).next() { + if let Some(value) = element.value().attr("href") { + return Some(value.to_string()); + } + } + + None +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..31f72e8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,15 @@ +pub mod html; +pub mod og; +pub mod preview; +pub mod schema; +pub mod twitter; + +pub use preview::{html_from_bytes, LinkPreview}; + +#[cfg(test)] +mod tests { + pub const FULL_FEATURED_HTML: &[u8] = include_bytes!("../html/full_featured.html"); + pub const OG_COMPILANT_HTML: &[u8] = include_bytes!("../html/og_compilant.html"); + pub const SCHEMA_COMPILANT_HTML: &[u8] = include_bytes!("../html/schema_compilant.html"); + pub const TWITTER_COMPILANT_HTML: &[u8] = include_bytes!("../html/twitter_compilant.html"); +} diff --git a/src/og.rs b/src/og.rs new file mode 100644 index 0000000..ff1bac1 --- /dev/null +++ b/src/og.rs @@ -0,0 +1,168 @@ +//! Open Graph Protocol bindings +//! +//! The Open Graph protocol enables any web page to become a rich object in a +//! social graph. For instance, this is used on Facebook to allow any web page +//! to have the same functionality as any other object on Facebook. +//! +//! +//! +//! # References +//! - [Official Documentation](https://ogp.me) +use scraper::{Html, Selector}; +use std::fmt; + +#[derive(Debug)] +pub struct OpenGraph { + title: Option, + description: Option, + url: Option, + image: Option, + object_type: Option, + locale: Option, +} + +/// OpenGraphTag meta tags collection +pub enum OpenGraphTag { + /// Represents the "og:title" OpenGraph meta tag. + /// + /// The title of your object as it should appear within + /// the graph, e.g., "The Rock". + Title, + /// Represents the "og:url" OpenGraph meta tag + Url, + /// Represents the "og:image" OpenGraph meta tag + Image, + /// Represents the "og:type" OpenGraph meta tag + /// + /// The type of your object, e.g., "video.movie". Depending on the type + /// you specify, other properties may also be required. + Type, + /// Represents the "og:description" OpenGraph meta tag + Description, + /// Represents the "og:locale" OpenGraph meta tag + Locale, + /// Represents the "og:image:height" OpenGraph meta tag + ImageHeight, + /// Represents the "og:image:width" OpenGraph meta tag + ImageWidth, + /// Represents the "og:site_name" OpenGraph meta tag + SiteName, +} + +impl fmt::Debug for OpenGraphTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.str()) + } +} + +impl OpenGraphTag { + fn str(&self) -> &str { + match self { + OpenGraphTag::Title => "title", + OpenGraphTag::Url => "url", + OpenGraphTag::Image => "image", + OpenGraphTag::Type => "type", + OpenGraphTag::Description => "description", + OpenGraphTag::Locale => "locale", + OpenGraphTag::ImageHeight => "image:height", + OpenGraphTag::ImageWidth => "image:width", + OpenGraphTag::SiteName => "site_name", + } + } +} + +/// Finds the OpenGraphTag tag specified in the provided `Html` instance +pub fn find_og_tag(html: &Html, tag: OpenGraphTag) -> Option { + let selector = Selector::parse(&format!("meta[property=\"og:{}\"]", tag.str())).unwrap(); + + if let Some(element) = html.select(&selector).next() { + if let Some(value) = element.value().attr("content") { + return Some(value.to_string()); + } + } + + None +} + +#[cfg(test)] +mod tests { + use crate::html_from_bytes; + use crate::tests::OG_COMPILANT_HTML; + + use super::{find_og_tag, OpenGraphTag}; + + #[test] + fn retrieves_title_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let title = find_og_tag(&html, OpenGraphTag::Title).unwrap(); + + assert_eq!(title, "SEO Strategies for a better web"); + } + + #[test] + fn retrieves_description_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let description = find_og_tag(&html, OpenGraphTag::Description).unwrap(); + + assert_eq!(description, "John Appleseed tells you his secrets on SEO for a better web experience by taking advantage of OpenGraph\'s Tags!"); + } + + #[test] + fn retrieves_url_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let url = find_og_tag(&html, OpenGraphTag::Url).unwrap(); + + assert_eq!(url, "https://abetterweb.com"); + } + + #[test] + fn retrieves_locale_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let locale = find_og_tag(&html, OpenGraphTag::Locale).unwrap(); + + assert_eq!(locale, "en_US"); + } + + #[test] + fn retrieves_image_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let image = find_og_tag(&html, OpenGraphTag::Image).unwrap(); + + assert_eq!( + image, + "https://www.apple.com/ac/structured-data/images/open_graph_logo.png?201809210816" + ); + } + + #[test] + fn retrieves_type_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let r#type = find_og_tag(&html, OpenGraphTag::Type).unwrap(); + + assert_eq!(r#type, "website"); + } + + #[test] + fn retrieves_image_height_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let height = find_og_tag(&html, OpenGraphTag::ImageHeight).unwrap(); + + assert_eq!(height, "600"); + } + + #[test] + fn retrieves_image_width_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let width = find_og_tag(&html, OpenGraphTag::ImageWidth).unwrap(); + + assert_eq!(width, "1200"); + } + + #[test] + fn retrieves_site_name_from_og_compilant_html() { + let html = html_from_bytes(OG_COMPILANT_HTML).unwrap(); + let site_name = find_og_tag(&html, OpenGraphTag::SiteName).unwrap(); + + assert_eq!(site_name, "TechPro"); + } +} diff --git a/src/preview.rs b/src/preview.rs new file mode 100644 index 0000000..c35412a --- /dev/null +++ b/src/preview.rs @@ -0,0 +1,291 @@ +#[cfg(feature = "fetch")] +use reqwest::get; + +use scraper::Html; +use std::str::FromStr; +use std::string::FromUtf8Error; +use thiserror::Error; +use url::Url; + +use crate::html::{find_link, find_meta_tag, first_inner_html}; +use crate::og::{find_og_tag, OpenGraphTag}; +use crate::schema::{find_schema_tag, SchemaMetaTag}; +use crate::twitter::{find_twitter_tag, TwitterMetaTag}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("The provided byte slice contains invalid UTF-8 characters")] + InvalidUtf8(FromUtf8Error), +} + +#[derive(Debug)] +pub struct LinkPreview { + pub title: Option, + pub description: Option, + pub domain: Option, + pub image_url: Option, +} + +impl LinkPreview { + /// Fetches the provided URL and retrieves an instance of `LinkPreview` + #[cfg(feature = "fetch")] + pub async fn fetch(url: &str) -> Self { + let resp = get(url).await.unwrap(); + let html = resp.text().await; + + LinkPreview::from_str(html) + } + + /// Attempts to find the description of the page in the following order: + /// + /// - Document's ` element's `href` attribute + /// - OpenGraphTag's image meta tag (`og:image`) + pub fn find_first_domain(html: &Html) -> Option { + if let Some(domain) = find_link(html, "canonical") { + return Some(domain); + } + + if let Some(domain) = find_og_tag(html, OpenGraphTag::Url) { + return Some(domain); + } + + None + } + + /// Attempts to find the description of the page in the following order: + /// + /// - OpenGraphTag's image meta tag (`og:image`) + /// - Document's ` element's `href` attribute + /// - Twitter Card's image meta tag (`twitter:image`) + /// - Schema.org image meta tag (`image`) + pub fn find_first_image_url(html: &Html) -> Option { + if let Some(image_url) = find_og_tag(html, OpenGraphTag::Image) { + return Some(image_url); + } + + if let Some(image_url) = find_link(html, "image_src") { + return Some(image_url); + } + + if let Some(image_url) = find_schema_tag(html, SchemaMetaTag::Image) { + return Some(image_url); + } + + if let Some(image_url) = find_twitter_tag(html, TwitterMetaTag::Image) { + return Some(image_url); + } + + None + } + + /// Attempts to find the description of the page in the following order: + /// + /// - OpenGraphTag's description meta tag (`og:description`) + /// - Twitter Card's description meta tag (`twitter:description`) + /// - Schema.org description meta tag (`description`) + /// - Description meta tag (`description`) + /// - The first `p` element from the document + pub fn find_first_description(html: &Html) -> Option { + if let Some(description) = find_og_tag(html, OpenGraphTag::Description) { + return Some(description); + } + + if let Some(description) = find_twitter_tag(html, TwitterMetaTag::Description) { + return Some(description); + } + + if let Some(description) = find_schema_tag(html, SchemaMetaTag::Description) { + return Some(description); + } + + if let Some(description) = find_meta_tag(html, "description") { + return Some(description); + } + + if let Some(description) = first_inner_html(html, "p") { + return Some(description); + } + + None + } + + /// Attempts to find the title of the page in the following order: + /// + /// - OpenGraphTag's title meta tag (`og:title`) + /// - Twitter Card's title meta tag (`twitter:title`) + /// - Schema.org title meta tag (`title`) + /// - The HTML's document title + /// - The first `

` tag in the document + /// - The first `

` tag in the document + pub fn find_first_title(html: &Html) -> Option { + if let Some(title) = find_og_tag(html, OpenGraphTag::Title) { + return Some(title); + } + + if let Some(title) = find_twitter_tag(html, TwitterMetaTag::Title) { + return Some(title); + } + + if let Some(title) = find_schema_tag(html, SchemaMetaTag::Name) { + return Some(title); + } + + if let Some(title) = first_inner_html(html, "title") { + return Some(title); + } + + if let Some(title) = first_inner_html(html, "h1") { + return Some(title); + } + + if let Some(title) = first_inner_html(html, "h2") { + return Some(title); + } + + None + } +} + +impl From for LinkPreview { + fn from(html: Html) -> Self { + let image_url: Option = + LinkPreview::find_first_image_url(&html).and_then(|url| url.parse::().ok()); + let domain: Option = + LinkPreview::find_first_domain(&html).and_then(|url| url.parse::().ok()); + + LinkPreview { + title: LinkPreview::find_first_title(&html), + description: LinkPreview::find_first_description(&html), + domain, + image_url, + } + } +} + +impl From<&Html> for LinkPreview { + fn from(html: &Html) -> Self { + let image_url: Option = + LinkPreview::find_first_image_url(html).and_then(|url| url.parse::().ok()); + let domain: Option = + LinkPreview::find_first_domain(html).and_then(|url| url.parse::().ok()); + + LinkPreview { + title: LinkPreview::find_first_title(html), + description: LinkPreview::find_first_description(html), + domain, + image_url, + } + } +} + +impl FromStr for LinkPreview { + type Err = Error; + + fn from_str(html: &str) -> Result { + let html = Html::parse_document(html); + let image_url: Option = + LinkPreview::find_first_image_url(&html).and_then(|url| url.parse::().ok()); + let domain: Option = + LinkPreview::find_first_domain(&html).and_then(|url| url.parse::().ok()); + + Ok(LinkPreview { + title: LinkPreview::find_first_title(&html), + description: LinkPreview::find_first_description(&html), + domain, + image_url, + }) + } +} + +/// Attempts to convert a HTML document byte slice into a HTML string instance +/// and then parses the document into a `Html` instance +pub fn html_from_bytes(value: &[u8]) -> Result { + let utf8 = String::from_utf8(value.to_vec()).map_err(Error::InvalidUtf8)?; + + Ok(Html::parse_document(utf8.as_str())) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::html_from_bytes; + use crate::tests::FULL_FEATURED_HTML; + + use super::LinkPreview; + + #[test] + fn creates_instance_of_link_preview_from_html_instance() { + let html = html_from_bytes(FULL_FEATURED_HTML).unwrap(); + let link_preview = LinkPreview::from(&html); + + assert_eq!( + link_preview.title.unwrap(), + "SEO Strategies for a better web" + ); + assert_eq!(link_preview.description.unwrap(), "John Appleseed tells you his secrets on SEO for a better web experience by taking advantage of OpenGraph\'s Tags!"); + assert_eq!( + link_preview.image_url.unwrap().to_string(), + "https://www.apple.com/ac/structured-data/images/open_graph_logo.png?201809210816" + ); + assert_eq!( + link_preview.domain.unwrap().to_string(), + "https://en.wikipedia.com/" + ); + } + + #[test] + fn creates_instance_of_link_preview_from_str_instance() { + let html = String::from_utf8(FULL_FEATURED_HTML.to_vec()).unwrap(); + let link_preview = LinkPreview::from_str(&html).unwrap(); + + assert_eq!( + link_preview.title.unwrap(), + "SEO Strategies for a better web" + ); + assert_eq!(link_preview.description.unwrap(), "John Appleseed tells you his secrets on SEO for a better web experience by taking advantage of OpenGraph\'s Tags!"); + assert_eq!( + link_preview.image_url.unwrap().to_string(), + "https://www.apple.com/ac/structured-data/images/open_graph_logo.png?201809210816" + ); + assert_eq!( + link_preview.domain.unwrap().to_string(), + "https://en.wikipedia.com/" + ); + } + + #[test] + fn finds_first_title() { + let html = html_from_bytes(FULL_FEATURED_HTML).unwrap(); + let title = LinkPreview::find_first_title(&html); + + assert_eq!(title.unwrap(), "SEO Strategies for a better web"); + } + + #[test] + fn finds_first_description() { + let html = html_from_bytes(FULL_FEATURED_HTML).unwrap(); + let title = LinkPreview::find_first_description(&html); + + assert_eq!(title.unwrap(), "John Appleseed tells you his secrets on SEO for a better web experience by taking advantage of OpenGraph\'s Tags!"); + } + + #[test] + fn finds_first_image_url() { + let html = html_from_bytes(FULL_FEATURED_HTML).unwrap(); + let title = LinkPreview::find_first_image_url(&html); + + assert_eq!( + title.unwrap(), + "https://www.apple.com/ac/structured-data/images/open_graph_logo.png?201809210816" + ); + } + + #[test] + fn finds_first_domain() { + let html = html_from_bytes(FULL_FEATURED_HTML).unwrap(); + let title = LinkPreview::find_first_domain(&html); + + assert_eq!(title.unwrap(), "https://en.wikipedia.com"); + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..caf1486 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,66 @@ +use scraper::{Html, Selector}; + +/// Schema.org meta tags. +pub enum SchemaMetaTag { + Name, + Description, + Image, +} + +impl SchemaMetaTag { + fn str(&self) -> &str { + match self { + SchemaMetaTag::Name => "name", + SchemaMetaTag::Description => "description", + SchemaMetaTag::Image => "image", + } + } +} + +/// Finds the Schema.org tag specified in the provided `Html` instance +pub fn find_schema_tag(html: &Html, tag: SchemaMetaTag) -> Option { + let selector = Selector::parse(&format!("meta[itemprop=\"{}\"]", tag.str())).unwrap(); + + if let Some(element) = html.select(&selector).next() { + if let Some(value) = element.value().attr("content") { + return Some(value.to_string()); + } + } + + None +} + +#[cfg(test)] +mod tests { + use crate::html_from_bytes; + use crate::tests::SCHEMA_COMPILANT_HTML; + + use super::{find_schema_tag, SchemaMetaTag}; + + #[test] + fn retrieves_schema_name() { + let html = html_from_bytes(SCHEMA_COMPILANT_HTML).unwrap(); + let value = find_schema_tag(&html, SchemaMetaTag::Name).unwrap(); + + assert_eq!(value, "Schema.org tags are awesome"); + } + + #[test] + fn retrieves_schema_description() { + let html = html_from_bytes(SCHEMA_COMPILANT_HTML).unwrap(); + let value = find_schema_tag(&html, SchemaMetaTag::Description).unwrap(); + + assert_eq!( + value, + "Tips to set Schema.org tags like you have been doing it since year 1" + ); + } + + #[test] + fn retrieves_schema_image() { + let html = html_from_bytes(SCHEMA_COMPILANT_HTML).unwrap(); + let value = find_schema_tag(&html, SchemaMetaTag::Image).unwrap(); + + assert_eq!(value, "https://www.example.com/image.jpg"); + } +} diff --git a/src/twitter.rs b/src/twitter.rs new file mode 100644 index 0000000..f8d6770 --- /dev/null +++ b/src/twitter.rs @@ -0,0 +1,102 @@ +use scraper::{Html, Selector}; + +/// Twittet meta tags. +pub enum TwitterMetaTag { + /// Title for the Twitter card + Title, + /// The card type, which will be one of “summary”, “summary_large_image”, + /// “app”, or “player”. + Card, + /// @username for the website used in the card footer. + /// This tag is not required, make sure the `Option::None` variant is handled. + Site, + /// @username for the content creator / author. + /// This tag is not required, make sure the `Option::None` variant is handled. + Creator, + /// Twitter Card Image + Image, + /// Card description + Description, +} + +impl TwitterMetaTag { + fn str(&self) -> &str { + match self { + TwitterMetaTag::Title => "title", + TwitterMetaTag::Card => "card", + TwitterMetaTag::Site => "site", + TwitterMetaTag::Creator => "creator", + TwitterMetaTag::Image => "image", + TwitterMetaTag::Description => "description", + } + } +} + +/// Finds the Twitter tag specified in the provided `Html` instance +pub fn find_twitter_tag(html: &Html, tag: TwitterMetaTag) -> Option { + let selector = Selector::parse(&format!("meta[name=\"twitter:{}\"]", tag.str())).unwrap(); + + if let Some(element) = html.select(&selector).next() { + if let Some(value) = element.value().attr("content") { + return Some(value.to_string()); + } + } + + None +} + +#[cfg(test)] +mod tests { + use crate::html_from_bytes; + use crate::tests::TWITTER_COMPILANT_HTML; + + use super::{find_twitter_tag, TwitterMetaTag}; + + #[test] + fn retrieves_card() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Card).unwrap(); + + assert_eq!(value, "summary_large_image"); + } + + #[test] + fn retrieves_title() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Title).unwrap(); + + assert_eq!(value, "SEO Strategies for a better web"); + } + + #[test] + fn retrieves_image() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Image).unwrap(); + + assert_eq!(value, "https://linktoyourimage"); + } + + #[test] + fn retrieves_description() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Description).unwrap(); + + assert_eq!(value, "John Appleseed tells you his secrets on SEO for a better web experience by taking advantage of OpenGraph's Tags!"); + } + + #[test] + fn retrieves_tweet_site_name() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Site).unwrap(); + + assert_eq!(value, "@nytimes"); + } + + #[test] + fn retrieves_tweet_creator_username() { + let html = html_from_bytes(TWITTER_COMPILANT_HTML).unwrap(); + let value = find_twitter_tag(&html, TwitterMetaTag::Creator).unwrap(); + + assert_eq!(value, "@EstebanBorai"); + } +}