diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index e8ca2dc..08d3f42 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -4,8 +4,11 @@ on: push: branches-ignore: - main + - 'release/**' + tags-ignore: + - '**' paths: - - '*.js' + - '**.js' jobs: lint_and_test: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4291ea2..61dac43 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -21,6 +21,7 @@ jobs: new_release_name: ${{ steps.new_release.outputs.name }} new_release_body: ${{ steps.new_release.outputs.body }} new_milestone_title: ${{ steps.new_milestone.outputs.title }} + new_milestone_description: ${{ steps.new_milestone.outputs.description }} steps: - name: Check pull request merged id: merged_pr @@ -79,7 +80,17 @@ jobs: if: steps.merged_pr.outputs.status == 'yes' id: new_milestone run: | - echo "title=$(echo "${{ steps.new_release.outputs.name }}" | awk -F . '{print $1 "." $2+1 "." "0"}')" >> $GITHUB_OUTPUT + next_release_version=$(echo "${{ steps.new_release.outputs.name }}" | awk -F . '{print $1 "." $2+1 "." "0"}') + + echo "title=${next_release_version}" >> $GITHUB_OUTPUT + + lf='\n' + description="## $(echo ${next_release_version} | sed -E 's/^v(.+)/\1/g') (202X/XX/XX)${lf}" + description+="${lf}### New features${lf}" + description+="${lf}### Enhancements${lf}" + description+="${lf}### Bug fixes${lf}" + + echo "description=${description}" >> $GITHUB_OUTPUT - name: Notify preparation run: | @@ -87,42 +98,11 @@ jobs: echo "New release name: ${{ steps.new_release.outputs.name }}" echo "New release body: ${{ steps.new_release.outputs.body }}" echo "New milestone title: ${{ steps.new_milestone.outputs.title }}" - - code_coverage: - needs: - - preparation - if: needs.preparation.outputs.pull_request_merged_status == 'yes' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout branch - uses: actions/checkout@v4 - - - name: Set up node - uses: actions/setup-node@v4 - with: - node-version: ${{ vars.DEFAULT_NODE_VERSION }} - cache: npm - cache-dependency-path: package-lock.json - - - name: Set up npm - run: | - npm ci - - - name: Publish code coverage - uses: paambaati/codeclimate-action@v5.0.0 - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - with: - coverageCommand: npm run test - debug: true + echo "New milestone description: ${{ steps.new_milestone.outputs.description }}" creation: needs: - preparation - - code_coverage if: needs.preparation.outputs.pull_request_merged_status == 'yes' runs-on: ubuntu-latest defaults: @@ -154,7 +134,8 @@ jobs: const params = { owner: context.repo.owner, repo: context.repo.repo, - title: '${{ needs.preparation.outputs.new_milestone_title }}' + title: '${{ needs.preparation.outputs.new_milestone_title }}', + description: '${{ needs.preparation.outputs.new_milestone_description }}' } await github.rest.issues.createMilestone(params) diff --git a/.github/workflows/ready-for-release.yml b/.github/workflows/ready-for-release.yml index 321e688..9556615 100644 --- a/.github/workflows/ready-for-release.yml +++ b/.github/workflows/ready-for-release.yml @@ -16,27 +16,14 @@ jobs: outputs: ready_for_release_labeled_status: ${{ steps.labeled_ready_for_release.outputs.status }} steps: - - name: Filter labeled name - id: filter_labeled - run: | - if [[ "${{ github.event.label.name }}" =~ ^ready-for-release ]]; then - i=0 - for matched in "${BASH_REMATCH[@]}"; do - if [ ${i} -eq 0 ]; then - echo "match=${matched}" >> $GITHUB_OUTPUT - else - echo "group${i}=${matched}" >> $GITHUB_OUTPUT - fi - i=$(expr ${i} + 1) - done - fi - - name: Check ready for release labeled id: labeled_ready_for_release run: | - if [ -n "${{ steps.filter_labeled.outputs.match }}" ] && [[ "${{ github.event.pull_request.head.ref }}" =~ ^release/.+ ]]; then - echo "status=yes" >> $GITHUB_OUTPUT - exit 0 + if [[ "${{ github.event.label.name }}" =~ ^ready-for-release ]] && [ "${#BASH_REMATCH[@]}" -gt 0 ]; then + if [[ "${{ github.event.pull_request.head.ref }}" =~ ^release/.+ ]]; then + echo "status=yes" >> $GITHUB_OUTPUT + exit 0 + fi fi echo "status=no" >> $GITHUB_OUTPUT @@ -54,7 +41,7 @@ jobs: shell: bash outputs: summarize_result: ${{ steps.summarize.outputs.result }} - list_up_outcomes: ${{ steps.list_up.outputs.outcomes }} + summarize_outcomes: ${{ steps.summarize.outputs.outcomes }} steps: - name: Checkout branch uses: actions/checkout@v4 @@ -82,19 +69,14 @@ jobs: run: | npm run test - - name: List up outcomes - id: list_up + - name: Summarize result + id: summarize run: | outcomes="{}" outcomes=$(echo "${outcomes}" | jq --argjson kv_pair "{\"lint\":\"${{ steps.lint.outcome }}\"}" '. + $kv_pair') outcomes=$(echo "${outcomes}" | jq --argjson kv_pair "{\"test\":\"${{ steps.test.outcome }}\"}" '. + $kv_pair') - - echo "outcomes=$(echo "${outcomes}" | jq -c)" >> $GITHUB_OUTPUT - - name: Summarize result - id: summarize - run: | - outcomes='${{ toJSON(fromJSON(steps.list_up.outputs.outcomes)) }}' + echo "outcomes=$(echo "${outcomes}" | jq -c)" >> $GITHUB_OUTPUT if "$(echo "${outcomes}" | jq -r '[values[] == ("failure")] | any')"; then echo "result=failure" >> $GITHUB_OUTPUT @@ -103,13 +85,12 @@ jobs: echo "result=skipped" >> $GITHUB_OUTPUT exit 0 fi - echo "result=success" >> $GITHUB_OUTPUT - name: Notify check application run: | echo "Summarize result: ${{ steps.summarize.outputs.result }}" - echo "List up outcomes: ${{ steps.list_up.outputs.outcomes }}" + echo "Summarize outcomes: ${{ steps.summarize.outputs.outcomes }}" check_milestone: needs: @@ -124,8 +105,9 @@ jobs: steps: - name: Check milestone set id: set_milestone + env: + milestone: ${{ toJSON(github.event.pull_request.milestone) }} run: | - milestone=$(echo '${{ toJSON(github.event.pull_request.milestone) }}' | jq) if [ "${milestone}" = "null" ]; then echo "status=no" >> $GITHUB_OUTPUT exit 0 @@ -185,7 +167,7 @@ jobs: id: pr_review_comment run: | lf='\n' - outcomes='${{ toJSON(fromJSON(needs.check_application.outputs.list_up_outcomes)) }}' + outcomes='${{ toJSON(fromJSON(needs.check_application.outputs.summarize_outcomes)) }}' message="## :information_source: リリース準備の結果通知" message+="${lf}### :memo: 概要" diff --git a/.github/workflows/upload-coverage.yml b/.github/workflows/upload-coverage.yml new file mode 100644 index 0000000..886473b --- /dev/null +++ b/.github/workflows/upload-coverage.yml @@ -0,0 +1,42 @@ +name: Upload coverage + +on: + push: + branches: + - main + - 'release/**' + tags: + - '!**' + +jobs: + lint_and_test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Set up node + uses: actions/setup-node@v4 + with: + node-version: ${{ vars.DEFAULT_NODE_VERSION }} + cache: npm + cache-dependency-path: package-lock.json + + - name: Set up npm + run: | + npm ci + + - name: Exec lint command + run: | + npm run lint + + - name: Publish code coverage + uses: paambaati/codeclimate-action@v5.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: npm run test + debug: true diff --git a/README.md b/README.md index 9b12f90..7b71246 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,29 @@ npm install hexo-tag-ogp-link-preview Write like below to your hexo article markdown file: ``` -{% link_preview url [target] [rel] %} +{% link_preview url [target] [rel] [loading] %} +[Content] +{% endlink_preview %} +``` + +or you are able to use "Named Parameter": + +``` +{% link_preview url [rel:{rel_value}] [target:{target_value}] [loading:{loading_value}] %} [Content] {% endlink_preview %} ``` ### Tag arguments -| Name | Required? | Default | Description | -|----------|-----------|------------|--------------------------------------------------------------------------------------------------------------------| -| `url` | Yes | | This parameter is a target of the link preview. | -| `target` | No | `_blank` | Specify a `target` attribute of the anchor element. One of `_self`, `_blank`, `_parent`, or `_top`. | -| `rel` | No | `nofollow` | Specify a `rel` attribute of the anchor element. One of `external`, `nofollow`, `noopener`, `noreferrer`, `opener` | +Notice: All optionally parameters (except for the required `url` parameter) are able to use "Named Parameter". + +| Name | Required? | Default | Description | +|-----------|-----------|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `url` | Yes | | This parameter is a target of the link preview. | +| `target` | No | `_blank` | Specify a `target` attribute of the anchor element.
One of `_self`, `_blank`, `_parent`, or `_top`. | +| `rel` | No | `nofollow` | Specify a `rel` attribute of the anchor element.
One of `external`, `nofollow`, `noopener`, `noreferrer`, or `opener`. | +| `loading` | No | `lazy` | Specify a `loading` attribute of the image element.
One of `lazy`, `eager`, or `none`.
If specify a `none`, remove loading attribute from image element. | ### Tag content @@ -51,21 +62,29 @@ link_preview: Notice: All setting values are NOT required. -| Name | type | Default | Description | -|----------------------------|----------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `class_name` | `string` or `object` | `link-preview` | If you are specified `string`, set a `class` attribute of the anchor element only. If you are specified `object`, set each a `class` attribute of the anchor element and the image element. | -| `class_name`.`anchor_link` | `string` | `link-preview` | Set a `class` attribute of the anchor element. | -| `class_name`.`image` | `string` | | Set a `class` attribute of the image element. If you are not specify (empty string, etc.), nothing to set. | -| `description_length` | `number` | `140` | It sliced to fit if a number of character of the `og:Description` exceeds the specified number value. | -| `disguise_crawler` | `boolean` | `true` | If scraper for OpenGraph want to disguise to crawler, set `true`. Otherwise, set to `false`. | +| Name | type | Default | Description | +|----------------------------|----------------------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `class_name` | `string` or `object` | `link-preview` | If you are specified `string`, set a `class` attribute of the anchor element only.
If you are specified `object`, set each a `class` attribute of the anchor element and the image element. | +| `class_name`.`anchor_link` | `string` | `link-preview` | Set a `class` attribute of the anchor element. | +| `class_name`.`image` | `string` | | Set a `class` attribute of the image element.
If you are not specify (empty string, etc.), nothing to set. | +| `description_length` | `number` | `140` | It sliced to fit if a number of character of the `og:Description` exceeds the specified number value. | +| `disguise_crawler` | `boolean` | `true` | If scraper for OpenGraph want to disguise to crawler, set `true`.
Otherwise, set to `false`. | ## Example +Write a following to your hexo article markdown file: + +```markdown +{% link_preview http://www.example.com/ loading:lazy %} +fallback Text +{% endlink_preview %} +``` + When scraper get OpenGraph successfully, generated html like blow: ```html
- + example image
title text
diff --git a/lib/common.js b/lib/common.js index b2fadfe..7622dc1 100644 --- a/lib/common.js +++ b/lib/common.js @@ -4,51 +4,21 @@ function hasProperty(obj, key) { return obj && Object.hasOwnProperty.call(obj, key); } -function stringLength(text) { - if (typeof text !== 'string') { - throw new Error('text is not string type.'); - } - - const segmentation = new Intl.Segmenter('ja', { granularity: 'grapheme' }); - return [...segmentation.segment(text)].length; +function hasTypeOfProperty(obj, key, type) { + return hasProperty(obj, key) && typeof obj[key] === type; } -function stringSlice(text, start, end) { - if (typeof text !== 'string') { - throw new Error('text is not string type.'); - } - - const strLength = stringLength(text); - - let startIndex = typeof start !== 'number' || isNaN(Number(start)) ? 0 : Number(start); - if (startIndex < 0) { - startIndex = Math.max(startIndex + strLength, 0); - } - - let endIndex = typeof end !== 'number' || isNaN(Number(end)) ? strLength : Number(end); - if (endIndex >= strLength) { - endIndex = strLength; - } else if (endIndex < 0) { - endIndex = Math.max(endIndex + strLength, 0); - } - - let strings = ''; +function getObjectValueFrom(obj, key, type, defaultValue) { + return hasTypeOfProperty(obj, key, type) ? obj[key] || defaultValue : defaultValue; +} - if (startIndex >= strLength || endIndex <= startIndex) { - return strings; - } - const segmentation = new Intl.Segmenter('ja', { granularity: 'grapheme' }); - [...segmentation.segment(text)] - .forEach((value, index) => { - if (startIndex <= index && index < endIndex) { - strings += value.segment; - } - }); - return strings; +function getValidNumber(value, defaultValue) { + return typeof value !== 'number' || isNaN(Number(value)) ? defaultValue : Number(value); } module.exports = { hasProperty, - stringLength, - stringSlice, + hasTypeOfProperty, + getObjectValueFrom, + getValidNumber, }; diff --git a/lib/configure.js b/lib/configure.js index fc3c962..38ca80e 100644 --- a/lib/configure.js +++ b/lib/configure.js @@ -1,49 +1,64 @@ 'use strict'; -const { hasProperty } = require('./common'); +const { hasTypeOfProperty, getObjectValueFrom } = require('./common'); const ANCHOR_LINK_CLASS_NAME = 'link-preview'; const DESCRIPTION_LENGTH = 140; const DISGUISE_CRAWLER = true; -module.exports = hexoCfg => { - const config = { - class_name: { - anchor_link: ANCHOR_LINK_CLASS_NAME, - }, +function getDefaultConfig() { + return { + class_name: { anchor_link: ANCHOR_LINK_CLASS_NAME }, description_length: DESCRIPTION_LENGTH, disguise_crawler: DISGUISE_CRAWLER, }; +} - if (!hasProperty(hexoCfg, 'link_preview')) { - return config; - } +function getClassNameObject(linkPreviewCfg, defaultValue) { + const classNameCfg = linkPreviewCfg.class_name; - const hexoCfgLinkPreview = hexoCfg.link_preview; + const classNameObj = { + anchor_link: getAnchorLinkClassName(classNameCfg, defaultValue), + }; - if (hasProperty(hexoCfgLinkPreview, 'class_name')) { - const hexoCfgLinkPreviewClassName = hexoCfgLinkPreview.class_name; + if (hasTypeOfProperty(classNameCfg, 'image', 'string') && classNameCfg.image !== '') { + classNameObj.image = classNameCfg.image; + } - if (typeof hexoCfgLinkPreviewClassName === 'string') { - config.class_name.anchor_link = hexoCfgLinkPreviewClassName || config.class_name.anchor_link; - } else if (typeof hexoCfgLinkPreviewClassName === 'object') { - if (hasProperty(hexoCfgLinkPreviewClassName, 'anchor_link') && typeof hexoCfgLinkPreviewClassName.anchor_link === 'string') { - config.class_name.anchor_link = hexoCfgLinkPreviewClassName.anchor_link || config.class_name.anchor_link; - } + return classNameObj; +} - if (hasProperty(hexoCfgLinkPreviewClassName, 'image') && hexoCfgLinkPreviewClassName.image !== '') { - config.class_name.image = hexoCfgLinkPreviewClassName.image; - } - } +function getAnchorLinkClassName(classNameCfg, defaultValue) { + switch (typeof classNameCfg) { + case 'object': + return getObjectValueFrom(classNameCfg, 'anchor_link', 'string', defaultValue); + case 'string': + return classNameCfg || defaultValue; } + return defaultValue; +} - if (hasProperty(hexoCfgLinkPreview, 'description_length') && typeof hexoCfgLinkPreview.description_length === 'number') { - config.description_length = hexoCfgLinkPreview.description_length || config.description_length; +function getDescriptionLength(linkPreviewCfg, defaultValue) { + return getObjectValueFrom(linkPreviewCfg, 'description_length', 'number', defaultValue); +} + +function getDisguiseCrawler(linkPreviewCfg, defaultValue) { + if (hasTypeOfProperty(linkPreviewCfg, 'disguise_crawler', 'boolean')) { + return linkPreviewCfg.disguise_crawler; } + return defaultValue; +} - if (hasProperty(hexoCfgLinkPreview, 'disguise_crawler') && typeof hexoCfgLinkPreview.disguise_crawler === 'boolean') { - config.disguise_crawler = hexoCfgLinkPreview.disguise_crawler; +module.exports = hexoCfg => { + if (!hasTypeOfProperty(hexoCfg, 'link_preview', 'object')) { + return getDefaultConfig(); } - return config; + const linkPreviewCfg = hexoCfg.link_preview; + + return { + class_name: getClassNameObject(linkPreviewCfg, ANCHOR_LINK_CLASS_NAME), + description_length: getDescriptionLength(linkPreviewCfg, DESCRIPTION_LENGTH), + disguise_crawler: getDisguiseCrawler(linkPreviewCfg, DISGUISE_CRAWLER), + }; }; diff --git a/lib/generator.js b/lib/generator.js index 44c855d..28f3632 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -19,7 +19,7 @@ module.exports = (scraper, params) => { const desc = newHtmlDivTag('og-description', escapedDesc); const descriptions = newHtmlDivTag('descriptions', title + desc); const image = isImageValid - ? newHtmlDivTag('og-image', newHtmlImgTag(imageUrl, params.generate)) : ''; + ? newHtmlDivTag('og-image', newHtmlImgTag(imageUrl, escapedTitle, params.generate)) : ''; const content = image + descriptions; return newHtmlAnchorTag(params.scrape.url, params.generate, content); diff --git a/lib/htmltag.js b/lib/htmltag.js index c749525..cec6d8d 100644 --- a/lib/htmltag.js +++ b/lib/htmltag.js @@ -1,7 +1,7 @@ 'use strict'; const util = require('hexo-util'); -const { hasProperty } = require('./common'); +const { hasTypeOfProperty } = require('./common'); function newHtmlDivTag(className, content) { return util.htmlTag('div', { class: className }, content, false); @@ -13,21 +13,25 @@ function newHtmlAnchorTag(url, config, content) { if (typeof content === 'string' && content !== '') { tagAttrs.class = config.className.anchor_link; return util.htmlTag('a', tagAttrs, content, false); - } else if (hasProperty(config, 'fallbackText') && typeof config.fallbackText === 'string' && config.fallbackText !== '') { + } + if (hasTypeOfProperty(config, 'fallbackText', 'string') && config.fallbackText !== '') { return util.htmlTag('a', tagAttrs, config.fallbackText); } throw new Error('failed to generate a new anchor tag.'); } -function newHtmlImgTag(url, config) { - const tagAttrs = { src: url }; +function newHtmlImgTag(url, alt, config) { + const tagAttrs = { src: url, alt: alt }; - if (hasProperty(config.className, 'image') && typeof config.className.image === 'string' && config.className.image !== '') { + if (hasTypeOfProperty(config.className, 'image', 'string') && config.className.image !== '') { tagAttrs.class = config.className.image; } + if (hasTypeOfProperty(config, 'loading', 'string') && ['lazy', 'eager'].includes(config.loading)) { + tagAttrs.loading = config.loading; + } - return util.htmlTag('img', tagAttrs, ''); + return util.htmlTag('img', tagAttrs); } module.exports = { diff --git a/lib/opengraph.js b/lib/opengraph.js index f30c7e4..4ae81f8 100644 --- a/lib/opengraph.js +++ b/lib/opengraph.js @@ -1,38 +1,29 @@ 'use strict'; const util = require('hexo-util'); -const { hasProperty, stringLength, stringSlice } = require('./common'); +const { hasProperty, hasTypeOfProperty, getObjectValueFrom } = require('./common'); +const { stringLength, stringSlice } = require('./strings'); function getOgTitle(ogp) { - const result = { valid: false, title: '' }; - - if (!hasProperty(ogp, 'ogTitle')) { - return result; + if (!hasTypeOfProperty(ogp, 'ogTitle', 'string')) { + return { valid: false, title: '' }; } const escapedTitle = util.escapeHTML(ogp.ogTitle); - if (typeof escapedTitle === 'string' && escapedTitle !== '') { - result.valid = true; - result.title = escapedTitle; - } - return result; + + return { valid: escapedTitle !== '', title: escapedTitle }; } function getOgDescription(ogp, maxLength) { - const result = { valid: false, description: '' }; - - if (!hasProperty(ogp, 'ogDescription')) { - return result; + if (!hasTypeOfProperty(ogp, 'ogDescription', 'string')) { + return { valid: false, description: '' }; } const escapedDescription = util.escapeHTML(ogp.ogDescription); const descriptionText = maxLength && stringLength(escapedDescription) > maxLength ? stringSlice(escapedDescription, 0, maxLength) + '...' : escapedDescription; - if (typeof descriptionText === 'string' && descriptionText !== '') { - result.valid = true; - result.description = descriptionText; - } - return result; + + return { valid: descriptionText !== '', description: descriptionText }; } function getOgImage(ogp, selectIndex = 0) { @@ -41,8 +32,9 @@ function getOgImage(ogp, selectIndex = 0) { } const index = selectIndex >= ogp.ogImage.length ? ogp.ogImage.length - 1 : 0; + const imageUrl = getObjectValueFrom(ogp.ogImage[index], 'url', 'string', ''); - return { valid: true, image: ogp.ogImage[index].url }; + return { valid: imageUrl !== '', image: imageUrl }; } module.exports = { diff --git a/lib/parameters.js b/lib/parameters.js index 3857ee3..67e4e59 100644 --- a/lib/parameters.js +++ b/lib/parameters.js @@ -1,25 +1,34 @@ 'use strict'; const urlRegex = /^(http|https):\/\//g; -const targetKeywords = ['_self', '_blank', '_parent', '_top']; -const relKeywords = ['external', 'nofollow', 'noopener', 'noreferrer', 'opener']; +const targetKeywords = ['_blank', '_self', '_parent', '_top']; +const relKeywords = ['nofollow', 'external', 'noopener', 'noreferrer', 'opener']; +const loadingKeywords = ['lazy', 'eager']; const CRAWLER_USER_AGENT = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/112.0.0.0 Safari/537.36'; function parseArgs(args) { - const urls = args.filter(arg => arg.search(urlRegex) === 0); - if (urls.length < 1) { + const [url, ...opts] = args; + + if (typeof url !== 'string' || url.search(urlRegex) < 0) { throw new Error('Scraping target url is not contains.'); } - const targets = args.filter(arg => targetKeywords.includes(arg)); - const relationships = args.filter(arg => relKeywords.includes(arg)); + const target = parseOptionalKeywordArg(opts, { argName: 'target:', index: 0 }, targetKeywords); + const rel = parseOptionalKeywordArg(opts, { argName: 'rel:', index: 1 }, relKeywords); + const loading = parseOptionalKeywordArg(opts, { argName: 'loading:', index: 2 }, loadingKeywords); - return { - url: urls[0], - target: targets[0] || '_blank', - rel: relationships[0] || 'nofollow', - }; + return { url, target, rel, loading }; +} + +function parseOptionalKeywordArg(opts, find, keywords, defaultIndex = 0) { + return findOptionalArgs(opts, find).filter(arg => keywords.includes(arg)).shift() || keywords[defaultIndex]; +} + +function findOptionalArgs(opts, find) { + const { argName, index } = find; + const args = opts.filter(arg => arg.startsWith(argName)).map(arg => arg.replace(argName, '')); + return args.length ? args : [opts[index]]; } function getFetchOptions(isCrawler) { @@ -38,12 +47,12 @@ function getFetchOptions(isCrawler) { } module.exports = (args, content, config) => { - const { url, target, rel } = parseArgs(args); + const { url, target, rel, loading } = parseArgs(args); const { class_name: className, description_length: descriptionLength, disguise_crawler: isCrawler } = config; const fetchOptions = getFetchOptions(isCrawler); return { scrape: { url, fetchOptions }, - generate: { target, rel, descriptionLength, className, fallbackText: content }, + generate: { target, rel, loading, descriptionLength, className, fallbackText: content }, }; }; diff --git a/lib/strings.js b/lib/strings.js new file mode 100644 index 0000000..d70eef2 --- /dev/null +++ b/lib/strings.js @@ -0,0 +1,53 @@ +'use strict'; + +const { getValidNumber } = require('./common'); + +function getStringSegmentOf(text) { + const segmentation = new Intl.Segmenter('ja', { granularity: 'grapheme' }); + return [...segmentation.segment(text)]; +} + +function stringLength(text) { + if (typeof text !== 'string') { + throw new Error('text is not string type.'); + } + + return getStringSegmentOf(text).length; +} + +function getStartIndex(start, strLength) { + const startIndex = getValidNumber(start, 0); + return startIndex < 0 ? Math.max(startIndex + strLength, 0) : startIndex; +} + +function getEndIndex(end, strLength) { + const endIndex = getValidNumber(end, strLength); + if (endIndex >= strLength) { + return strLength; + } + return endIndex < 0 ? Math.max(endIndex + strLength, 0) : endIndex; +} + +function stringSlice(text, start, end) { + if (typeof text !== 'string') { + throw new Error('text is not string type.'); + } + + const strLength = stringLength(text); + const startIndex = getStartIndex(start, strLength); + const endIndex = getEndIndex(end, strLength); + + if (startIndex >= strLength || endIndex <= startIndex) { + return ''; + } + return getStringSegmentOf(text) + .map((element, index) => { + return startIndex <= index && index < endIndex ? element.segment : ''; + }) + .join(''); +} + +module.exports = { + stringLength, + stringSlice, +}; diff --git a/package.json b/package.json index 0c588d5..bb2ee8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hexo-tag-ogp-link-preview", "description": "A Hexo tag plugin for embedding link preview by OpenGraph on article.", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "author": "Hiroki Sugawara ", "homepage": "https://blog.chaotic-notes.com/", diff --git a/test/common.test.js b/test/common.test.js index 11410a1..f56b684 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { hasProperty, stringLength, stringSlice } = require('../lib/common'); +const { hasProperty, hasTypeOfProperty, getObjectValueFrom, getValidNumber } = require('../lib/common'); describe('common', () => { it('Has a property at object', () => { @@ -15,39 +15,35 @@ describe('common', () => { expect(hasProperty(obj, 'not_test_key')).toBeFalsy(); }); - it('Calculate a length of string', () => { - expect(stringLength('aBあア亞  19%+👨🏻‍💻🇯🇵🍎')).toEqual(14); - }); + it('Has a property at object which string type', () => { + const obj = { 'test_key': 'test_value' }; - it('Cannot calculate a length of except for string', () => { - expect(() => stringLength(0)).toThrow(new Error('text is not string type.')); + expect(hasTypeOfProperty(obj, 'test_key', 'string')).toBeTruthy(); }); - it('Slice a string', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 0, 3)).toEqual('aBあ'); - }); + it('Has a property at object which except for string type', () => { + const obj = { 'test_key': 1234 }; - it('Cannot slice a value except for string', () => { - expect(() => stringSlice(0)).toThrow(new Error('text is not string type.')); + expect(hasTypeOfProperty(obj, 'test_key', 'string')).toBeFalsy(); }); - it('Slice a string (start is not number type)', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 'hoge', 5)).toEqual('aBあア亞'); - }); + it('Get a value of property at object which string type successfully', () => { + const obj = { 'test_key': 'test_value' }; - it('Slice a string (start is smaller than zero)', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', -5, 13)).toEqual('%+👨🏻‍💻🇯🇵'); + expect(getObjectValueFrom(obj, 'test_key', 'string', 'default_value')).toEqual('test_value'); }); - it('Slice a string (end calculate automatically)', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 7)).toEqual('19%+👨🏻‍💻🇯🇵🍎'); + it('Get a default value which specified because fail to get a value of object property', () => { + const obj = { 'test_key': 1234 }; + + expect(getObjectValueFrom(obj, 'test_key', 'string', 'default_value')).toEqual('default_value'); }); - it('Slice a string (end is smaller than zero)', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 7, -4)).toEqual('19%'); + it('Get a valid number from specified value successfully', () => { + expect(getValidNumber(1234, 9876)).toEqual(1234); }); - it('Slice a string (end is smaller than start)', () => { - expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', -1, -2)).toEqual(''); + it('Get a default number which specified because value is not valid number', () => { + expect(getValidNumber('test_value', 9876)).toEqual(9876); }); }); diff --git a/test/configure.test.js b/test/configure.test.js index bf43d92..123068d 100644 --- a/test/configure.test.js +++ b/test/configure.test.js @@ -18,7 +18,7 @@ describe('configure', () => { it('Specify all values', () => { const hexoCfg = { link_preview: { - class_name: { anchor_link: 'link-preview', image: 'not-gallery-item' }, + class_name: { anchor_link: 'link-preview-2', image: 'not-gallery-item' }, description_length: 100, disguise_crawler: false, }, @@ -26,7 +26,7 @@ describe('configure', () => { expect(getConfig(hexoCfg)).toEqual( { - class_name: { anchor_link: 'link-preview', image: 'not-gallery-item' }, + class_name: { anchor_link: 'link-preview-2', image: 'not-gallery-item' }, description_length: 100, disguise_crawler: false, } @@ -36,13 +36,13 @@ describe('configure', () => { it('Specify a valid string value at class_name', () => { const hexoCfg = { link_preview: { - class_name: 'link-preview', + class_name: 'link-preview-2', }, }; expect(getConfig(hexoCfg)).toEqual( { - class_name: { anchor_link: 'link-preview' }, + class_name: { anchor_link: 'link-preview-2' }, description_length: 140, disguise_crawler: true, } @@ -68,14 +68,14 @@ describe('configure', () => { it('Specify a object which has valid anchor_link only at class_name', () => { const hexoCfg = { link_preview: { - class_name: { anchor_link: 'link-preview' }, + class_name: { anchor_link: 'link-preview-2' }, }, }; expect(getConfig(hexoCfg)).toEqual( { class_name: { - anchor_link: 'link-preview', + anchor_link: 'link-preview-2', }, description_length: 140, disguise_crawler: true, diff --git a/test/generator.test.js b/test/generator.test.js index 254ce9e..e86fd5d 100644 --- a/test/generator.test.js +++ b/test/generator.test.js @@ -28,7 +28,7 @@ describe('generator', () => { params.generate, newHtmlDivTag( 'og-image', - newHtmlImgTag(imageUrl, params.generate) + newHtmlImgTag(imageUrl, title, params.generate) ) + newHtmlDivTag( 'descriptions', diff --git a/test/htmltag.test.js b/test/htmltag.test.js index 6d70c74..0aeb77d 100644 --- a/test/htmltag.test.js +++ b/test/htmltag.test.js @@ -53,21 +53,39 @@ describe('htmlTag', () => { it('Generate a new html image tag', () => { const url = 'http://example.com/'; + const alt = 'alternative text'; const config = { className: { image: 'image-class' }, + loading: 'lazy', }; - expect(newHtmlImgTag(url, config)).toEqual( - `` + expect(newHtmlImgTag(url, alt, config)).toEqual( + `${alt}` ); }); it('Generate a new html image tag without class name', () => { const url = 'http://example.com/'; - const config = {}; + const alt = 'alternative text'; + const config = { + loading: 'eager', + }; + + expect(newHtmlImgTag(url, alt, config)).toEqual( + `${alt}` + ); + }); + + it('Generate a new html image tag without loading', () => { + const url = 'http://example.com/'; + const alt = 'alternative text'; + const config = { + className: { image: 'image-class' }, + loading: 'none', + }; - expect(newHtmlImgTag(url, config)).toEqual( - `` + expect(newHtmlImgTag(url, alt, config)).toEqual( + `${alt}` ); }); }); diff --git a/test/opengraph.test.js b/test/opengraph.test.js index 7bd2beb..690f5b5 100644 --- a/test/opengraph.test.js +++ b/test/opengraph.test.js @@ -4,110 +4,98 @@ const { getOgTitle, getOgDescription, getOgImage } = require('../lib/opengraph') describe('opengraph', () => { it('Has a valid value at OpenGraphTitle', () => { - const ogp = { - 'ogTitle': 'test_title', - }; + const ogp = { 'ogTitle': 'test_title' }; - expect(getOgTitle(ogp)).toEqual( - { valid: true, title: 'test_title' } - ); + expect(getOgTitle(ogp)).toEqual({ valid: true, title: 'test_title' }); }); it('Not contains a value of OpenGraphTitle', () => { const ogp = {}; - expect(getOgTitle(ogp)).toEqual( - { valid: false, title: '' } - ); + expect(getOgTitle(ogp)).toEqual({ valid: false, title: '' }); + }); + + it('Has a empty value at OpenGraphTitle', () => { + const ogp = { 'ogTitle': '' }; + + expect(getOgTitle(ogp)).toEqual({ valid: false, title: '' }); }); it('Has a invalid value at OpenGraphTitle', () => { - const ogp = { - 'ogTitle': '', - }; + const ogp = { 'ogTitle': 1000 }; - expect(getOgTitle(ogp)).toEqual( - { valid: false, title: '' } - ); + expect(getOgTitle(ogp)).toEqual({ valid: false, title: '' }); }); it('Has a valid value at OpenGraphDescription', () => { - const ogp = { - 'ogDescription': 'test_description', - }; + const ogp = { 'ogDescription': 'test_description' }; - expect(getOgDescription(ogp, 140)).toEqual( - { valid: true, description: 'test_description' } - ); + expect(getOgDescription(ogp, 140)).toEqual({ valid: true, description: 'test_description' }); }); it('Has a valid english sentence which is over max length at OpenGraphDescription', () => { - const ogp = { - 'ogDescription': 'test_description', - }; + const ogp = { 'ogDescription': 'test_description' }; - expect(getOgDescription(ogp, 4)).toEqual( - { valid: true, description: 'test...' } - ); + expect(getOgDescription(ogp, 4)).toEqual({ valid: true, description: 'test...' }); }); it('Has a valid japanese sentence which is over max length at OpenGraphDescription', () => { - const ogp = { - 'ogDescription': '👨🏻‍💻󠁧󠁢󠁥󠁮󠁧󠁿テストの🇯🇵ですくりぷしょん🍎を書きました。', - }; + const ogp = { 'ogDescription': '👨🏻‍💻󠁧󠁢󠁥󠁮󠁧󠁿テストの🇯🇵ですくりぷしょん🍎を書きました。' }; - expect(getOgDescription(ogp, 15)).toEqual( - { valid: true, description: '👨🏻‍💻󠁧󠁢󠁥󠁮󠁧󠁿テストの🇯🇵ですくりぷしょん🍎...' } - ); + expect(getOgDescription(ogp, 15)).toEqual({ valid: true, description: '👨🏻‍💻󠁧󠁢󠁥󠁮󠁧󠁿テストの🇯🇵ですくりぷしょん🍎...' }); }); it('Not contains a value of OpenGraphDescription', () => { const ogp = {}; - expect(getOgDescription(ogp, 140)).toEqual( - { valid: false, description: '' } - ); + expect(getOgDescription(ogp, 140)).toEqual({ valid: false, description: '' }); + }); + + it('Has a empty value at OpenGraphDescription', () => { + const ogp = { 'ogDescription': '' }; + + expect(getOgDescription(ogp, 140)).toEqual({ valid: false, description: '' }); }); it('Has a invalid value at OpenGraphDescription', () => { - const ogp = { - 'ogDescription': '', - }; + const ogp = { 'ogDescription': 1234 }; - expect(getOgDescription(ogp, 140)).toEqual( - { valid: false, description: '' } - ); + expect(getOgDescription(ogp, 140)).toEqual({ valid: false, description: '' }); }); it('Has a value at OpenGraphImage', () => { - const ogp = { - 'ogImage': [ - { url: 'test.png' }, - ], - }; + const ogp = { 'ogImage': [{ url: 'test.png' }]}; - expect(getOgImage(ogp)).toEqual( - { valid: true, image: 'test.png' } - ); + expect(getOgImage(ogp)).toEqual({ valid: true, image: 'test.png' }); + }); + + it('Has a empty value at OpenGraphImage', () => { + const ogp = { 'ogImage': [{ url: '' }]}; + + expect(getOgImage(ogp)).toEqual({ valid: false, image: '' }); + }); + + it('Has a invalid value at OpenGraphImage', () => { + const ogp = { 'ogImage': [{ url: 1234 }]}; + + expect(getOgImage(ogp)).toEqual({ valid: false, image: '' }); }); it('Has a value at OpenGraphImage (selectIndex is larger than length)', () => { - const ogp = { - 'ogImage': [ - { url: 'test.png' }, - ], - }; + const ogp = { 'ogImage': [{ url: 'test.png' }]}; - expect(getOgImage(ogp, 2)).toEqual( - { valid: true, image: 'test.png' } - ); + expect(getOgImage(ogp, 2)).toEqual({ valid: true, image: 'test.png' }); }); it('Not contains a value of OpenGraphImage', () => { const ogp = {}; - expect(getOgImage(ogp)).toEqual( - { valid: false, image: '' } - ); + expect(getOgImage(ogp)).toEqual({ valid: false, image: '' }); + }); + + it('Not contains a url value in OpenGraphImage', () => { + const ogp = { 'ogImage': [{ width: 1200 }]}; + + expect(getOgImage(ogp)).toEqual({ valid: false, image: '' }); }); }); diff --git a/test/parameters.test.js b/test/parameters.test.js index a8d7521..40f64a4 100644 --- a/test/parameters.test.js +++ b/test/parameters.test.js @@ -4,7 +4,7 @@ const getParameters = require('../lib/parameters'); describe('parameters', () => { it('Specify all arguments explicitly', () => { - const args = ['https://example.com', '_blank', 'nofollow']; + const args = ['https://example.com', '_self', 'rel:noopener', 'loading:eager']; const fallbackText = 'fallbackText'; const config = { class_name: { anchor_link: 'link-preview' }, descriptionLength: 140, disguise_crawler: true }; const { class_name: className, description_length: descriptionLength } = config; @@ -19,7 +19,14 @@ describe('parameters', () => { }, }, }, - generate: { target: args[1], rel: args[2], descriptionLength, className, fallbackText }, + generate: { + target: args[1], + rel: args[2].replace('rel:', ''), + loading: args[3].replace('loading:', ''), + descriptionLength, + className, + fallbackText, + }, } ); }); @@ -40,7 +47,7 @@ describe('parameters', () => { }, }, }, - generate: { target: '_blank', rel: 'nofollow', descriptionLength, className, fallbackText }, + generate: { target: '_blank', rel: 'nofollow', loading: 'lazy', descriptionLength, className, fallbackText }, } ); }); @@ -54,7 +61,7 @@ describe('parameters', () => { expect(getParameters(args, fallbackText, config)).toEqual( { scrape: { url: args[0], fetchOptions: {} }, - generate: { target: '_blank', rel: 'nofollow', descriptionLength, className, fallbackText }, + generate: { target: '_blank', rel: 'nofollow', loading: 'lazy', descriptionLength, className, fallbackText }, } ); }); diff --git a/test/strings.test.js b/test/strings.test.js new file mode 100644 index 0000000..5beac9f --- /dev/null +++ b/test/strings.test.js @@ -0,0 +1,41 @@ +'use strict'; + +const { stringLength, stringSlice } = require('../lib/strings'); + +describe('common', () => { + it('Calculate a length of string', () => { + expect(stringLength('aBあア亞  19%+👨🏻‍💻🇯🇵🍎')).toEqual(14); + }); + + it('Cannot calculate a length of except for string', () => { + expect(() => stringLength(0)).toThrow(new Error('text is not string type.')); + }); + + it('Slice a string', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 0, 3)).toEqual('aBあ'); + }); + + it('Cannot slice a value except for string', () => { + expect(() => stringSlice(0)).toThrow(new Error('text is not string type.')); + }); + + it('Slice a string (start is not number type)', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 'hoge', 5)).toEqual('aBあア亞'); + }); + + it('Slice a string (start is smaller than zero)', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', -5, 13)).toEqual('%+👨🏻‍💻🇯🇵'); + }); + + it('Slice a string (end calculate automatically)', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 7)).toEqual('19%+👨🏻‍💻🇯🇵🍎'); + }); + + it('Slice a string (end is smaller than zero)', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', 7, -4)).toEqual('19%'); + }); + + it('Slice a string (end is smaller than start)', () => { + expect(stringSlice('aBあア亞  19%+👨🏻‍💻🇯🇵🍎', -1, -2)).toEqual(''); + }); +});