diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000..121b436 --- /dev/null +++ b/build.config.ts @@ -0,0 +1,11 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + rollup: { + emitCJS: true, + }, + rootDir: './lib', + outDir: '../dist', + entries: ['index.js'], + declaration: true, +}) diff --git a/dist/index.cjs b/dist/index.cjs new file mode 100644 index 0000000..5b610b6 --- /dev/null +++ b/dist/index.cjs @@ -0,0 +1,166 @@ +'use strict'; + +const postcss = require('postcss'); +const isUrl = require('is-url-superb'); +const defu = require('defu'); +const api_js = require('posthtml/lib/api.js'); +const srcset = require('srcset'); + +function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } + +const postcss__default = /*#__PURE__*/_interopDefaultCompat(postcss); +const isUrl__default = /*#__PURE__*/_interopDefaultCompat(isUrl); + +const urlPattern = /(url\(["']?)(.*?)(["']?\))/g; +function postcssBaseurl(options = {}) { + const { base } = options; + return { + postcssPlugin: "postcss-baseurl", + Once(root) { + root.walkAtRules((rule) => { + if (rule.name === "font-face") { + rule.walkDecls((decl) => { + const { value } = decl; + decl.value = value.replace(urlPattern, ($0, $1, $2, $3) => { + return isUrl__default($2) ? $1 + $2 + $3 : $1 + base + $2 + $3; + }); + }); + } + }); + root.walkRules((rule) => { + rule.walkDecls((decl) => { + const { value } = decl; + decl.value = value.replace(urlPattern, ($0, $1, $2, $3) => { + return isUrl__default($2) ? $1 + $2 + $3 : $1 + base + $2 + $3; + }); + }); + }); + } + }; +} +postcssBaseurl.postcss = true; + +const defaultTags = { + a: { + href: true + }, + area: { + href: true + }, + audio: { + src: true + }, + base: { + href: true + }, + body: { + background: true + }, + embed: { + src: true + }, + iframe: { + src: true + }, + img: { + src: true, + srcset: true + }, + input: { + src: true + }, + link: { + href: true + }, + script: { + src: true + }, + source: { + src: true, + srcset: true + }, + table: { + background: true + }, + td: { + background: true + }, + th: { + background: true + }, + track: { + src: true + }, + video: { + poster: true + } +}; +const plugin = (options = {}) => (tree) => { + options.url = options.url || ""; + options.attributes = options.attributes || {}; + options.allTags = options.allTags || false; + options.styleTag = options.styleTag || false; + options.inlineCss = options.inlineCss || false; + options.tags = options.allTags ? defu.defu(options.tags, defaultTags) : options.tags || {}; + tree.walk = tree.walk || api_js.walk; + const process = (node) => { + if (options.url && (typeof options.url !== "string" || options.url === "")) { + return node; + } + if (!["array", "object"].includes(typeof options.tags)) { + return node; + } + if (node.tag === "style" && node.content && options.styleTag) { + node.content = postcss__default([ + postcssBaseurl({ + base: options.url + }) + ]).process(node.content.join("").trim()).css; + } + if (node.attrs?.style && options.inlineCss) { + node.attrs.style = prependUrl(node.attrs.style, options.url); + } + for (const [attribute, value] of Object.entries(options.attributes)) { + if (node.attrs?.[attribute]) { + handleSingleValueAttributes(node, attribute, value); + } + } + const tags = Array.isArray(options.tags) ? Object.entries(defaultTags).filter(([tag]) => options.tags.includes(tag)) : Object.entries(options.tags); + tags.forEach(([tag, attributes]) => { + if (node.tag !== tag) { + return node; + } + for (const [attribute, value] of Object.entries(attributes)) { + if (node.attrs?.[attribute]) { + if (["href", "src", "poster", "background"].includes(attribute)) { + handleSingleValueAttributes(node, attribute, value); + } + if (attribute === "srcset") { + const parsed = srcset.parseSrcset(node.attrs[attribute]); + parsed.map((p) => { + if (!isUrl__default(p.url)) { + p.url = typeof value === "boolean" ? options.url + p.url : value + p.url; + } + return p; + }); + node.attrs[attribute] = srcset.stringifySrcset(parsed); + } + } + } + }); + return node; + }; + const handleSingleValueAttributes = (node, attribute, value) => { + if (isUrl__default(node.attrs[attribute])) { + return node; + } + node.attrs[attribute] = typeof value === "boolean" ? options.url + node.attrs[attribute] : value + node.attrs[attribute]; + }; + const prependUrl = (value, url) => { + const { css } = postcss__default([postcssBaseurl({ base: url })]).process(`div { ${value} }`); + return css.replace(/div {\s|\s}$/gm, ""); + }; + return tree.walk(process); +}; + +module.exports = plugin; diff --git a/dist/index.d.cts b/dist/index.d.cts new file mode 100644 index 0000000..661a4b0 --- /dev/null +++ b/dist/index.d.cts @@ -0,0 +1,88 @@ +type BaseURLConfig = { + /** + The URL string to prepend. + + @default '' + + @example + + ``` + baseUrl({ + url: 'https://example.com/', + }) + ``` + */ + url: string; + + /** + Tags to apply the `url` to. When using this option, the `url` will only be prepended to the specified tags. + + @example + + Using an array of strings representing tag names: + + ``` + baseUrl({ + url: 'https://cdn.example.com/', + tags: ['img'], + }) + ``` + + Using an object to specify tags and their attributes to apply the `url` to: + + ``` + baseUrl({ + url: 'https://foo.com/', + tags: { + img: { + src: true, + srcset: 'https://bar.com/', + }, + }, + }) + ``` + */ + tags?: string[] | Record; + + /** + Key-value pairs of attributes and the string to prepend to their existing value. + + @default {} + + @example + + Prepend `https://example.com/` to all `data-url` attribute values: + + ``` + baseUrl({ + attributes: { + 'data-url': 'https://example.com/', + } + }) + ``` + */ + attributes?: Record; + + /** + Whether the string defined in `url` should be prepended to `url()` values in CSS `