Skip to content

Commit

Permalink
chore: update logic + include build
Browse files Browse the repository at this point in the history
  • Loading branch information
userquin committed Aug 6, 2023
1 parent 2857487 commit 22d1414
Show file tree
Hide file tree
Showing 7 changed files with 528 additions and 159 deletions.
9 changes: 9 additions & 0 deletions examples/vite-vue3/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
<div class="sprite-custom-chevron-up?bg" />
<div class="sprite-custom-animated?bg" />

<div>Custom MDI SVG Sprite</div>
<div class="sprite-custom-mdi-account" />
<div class="sprite-custom-mdi-alert-octagram" />
<div class="sprite-custom-mdi-access-point-network" />

<div>Custom SVG Sprite From file system</div>
<div class="sprite-custom-fs-icon" />
<div class="sprite-custom-fs-multi-line-attr" />

<div>Custom SVG Sprite (attributify)</div>
<div sprite-custom-close />
<div sprite-custom-chevron-down />
Expand Down
101 changes: 90 additions & 11 deletions examples/vite-vue3/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { resolve } from 'node:path'
import { basename, dirname, extname, resolve } from 'node:path'
import { opendir, readFile } from 'node:fs/promises'
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import presetAttributify from '@unocss/preset-attributify'
import presetIcons from '@unocss/preset-icons'
import presetUno from '@unocss/preset-uno'
import type { AsyncSpriteIconsFactory, SpriteIcon } from '@unocss/preset-icons'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import type { AutoInstall } from '@iconify/utils/lib/loader/fs'
import { loadCollectionFromFS } from '@iconify/utils/lib/loader/fs'
import { searchForIcon } from '@iconify/utils/lib/loader/modern'

const iconDirectory = resolve(__dirname, 'icons')

// https://vitejs.dev/config/
export default defineConfig({
base: '/app/',
plugins: [
Vue(),
UnoCSS({
Expand All @@ -30,11 +36,14 @@ export default defineConfig({
custom: FileSystemIconLoader(iconDirectory),
},
sprites: {
collections: ['custom'],
loader: (name) => {
if (name === 'custom') {
return {
'animated': `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><style>
sprites: {
'custom-mdi': createLoadCollectionFromFSAsyncIterator('mdi', {
include: ['account', 'alert-octagram', 'access-point-network'],
}),
'custom-fs': createFileSystemIconLoaderAsyncIterator('icons', 'custom-fs'),
'custom': <SpriteIcon[]>[{
name: 'animated',
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><style>
path.animated {
fill-opacity: 0;
animation: animated-test-animation 2s linear forwards;
Expand All @@ -48,15 +57,85 @@ export default defineConfig({
}
}
</style><path class="animated" fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>`,
'close': '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>',
'chevron-down': '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6l1.41-1.42Z"/></svg>',
'chevron-up': '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z"/></svg>',
}
}
}, {
name: 'close',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>',
}, {
name: 'chevron-down',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6l1.41-1.42Z"/></svg>',
}, {
name: 'chevron-up',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z"/></svg>',
}],
},
},
}),
],
}),
],
})

/* TODO BEGIN-CLEANUP: types from @iconify/utils: remove them once published */
function createLoadCollectionFromFSAsyncIterator(
collection: string,
options: {
autoInstall?: AutoInstall
include?: string[] | ((icon: string) => boolean)
} = { autoInstall: false },
) {
const include = options.include ?? (() => true)
const useInclude: (icon: string) => boolean
= typeof include === 'function'
? include
: (icon: string) => include.includes(icon)

return <AsyncSpriteIconsFactory> async function* () {
const iconSet = await loadCollectionFromFS(collection)
if (iconSet) {
const icons = Object.keys(iconSet.icons).filter(useInclude)
for (const id of icons) {
const iconData = await searchForIcon(
iconSet,
collection,
[id],
options,
)
if (iconData) {
yield {
name: id,
svg: iconData,
collection,
}
}
}
}
}
}
function createFileSystemIconLoaderAsyncIterator(
dir: string,
collection = dirname(dir),
include: string[] | ((icon: string) => boolean) = () => true,
) {
const useInclude: (icon: string) => boolean
= typeof include === 'function'
? include
: (icon: string) => include.includes(icon)

return <AsyncSpriteIconsFactory> async function* () {
const stream = await opendir(dir)
for await (const file of stream) {
if (!file.isFile() || extname(file.name) !== '.svg')
continue

const name = basename(file.name).slice(0, -4)
if (useInclude(name)) {
yield {
name,
svg: await readFile(resolve(dir, file.name), 'utf-8'),
collection,
}
}
}
}
}
/* TODO END-CLEANUP: types from @iconify/utils: remove them once published */
175 changes: 126 additions & 49 deletions packages/preset-icons/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import type { DynamicMatcher, Preset, Rule } from '@unocss/core'
import { warnOnce } from '@unocss/core'
import type {
CustomIconLoader,
IconifyLoaderOptions,
UniversalIconLoader,
} from '@iconify/utils/lib/loader/types'
import { encodeSvgForCss } from '@iconify/utils/lib/svg/encode-svg-for-css'

import { mergeIconProps, trimSVG } from '@iconify/utils'
import type { CSSSVGSprites, IconsOptions } from './types'
import { createAsyncSpriteIconsFactory, createUint8ArraySprite } from './create-sprite'

const COLLECTION_NAME_PARTS_MAX = 3

export { IconsOptions }

interface CSSSVGSpritesOptions extends CSSSVGSprites {
svgCollections: Record<string, Record<string, string>>
customCollections: Record<string, CustomIconLoader>
interface GeneratedSpriteData {
name: string
asset: Uint8Array
}

interface PresetIcons extends Preset {
generateCSSSVGSprites: () => AsyncIterableIterator<GeneratedSpriteData>
createCSSSVGSprite: (collection: string) => Promise<Uint8Array | undefined>
}

export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => Promise<UniversalIconLoader>) {
Expand Down Expand Up @@ -64,40 +70,9 @@ export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => P
]]

if (sprites) {
const collections = Array.isArray(sprites.collections) ? sprites.collections : [sprites.collections]
const svgCollections: Record<string, Record<string, string>> = {}
const originalLoader = sprites.loader
const customCollections = collections.reduce((acc, c) => {
acc[c] = async (name) => {
let collection: Record<string, string> | undefined = svgCollections[c]
if (!collection) {
collection = await originalLoader(c)
svgCollections[c] = collection ?? {}
}

return collection?.[name]
}

return acc
}, <Record<string, CustomIconLoader>>{})
const spriteOptions: CSSSVGSpritesOptions = {
...sprites,
// override loader to cache collections
async loader(name) {
let collection: Record<string, string> | undefined = svgCollections[name]
if (!collection) {
collection = await originalLoader(name)
svgCollections[name] = collection ?? {}
}

return collection
},
svgCollections,
customCollections,
}
rules.push([
/^([a-z0-9:_-]+)(?:\?(mask|bg|auto))?$/,
createDynamicMatcher(warn, sprites.mode ?? mode, loaderOptions, iconLoaderResolver, spriteOptions),
createDynamicMatcher(warn, sprites.mode ?? mode, loaderOptions, iconLoaderResolver, sprites),
{ layer, prefix: sprites.prefix ?? 'sprite-' },
])
}
Expand All @@ -109,12 +84,14 @@ export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => P
return iconLoader
}

return {
return <PresetIcons>{
name: '@unocss/preset-icons',
enforce: 'pre',
options,
layers: { icons: -30 },
rules,
generateCSSSVGSprites: createGenerateCSSSVGSprites(sprites),
createCSSSVGSprite: createCSSSVGSpriteLoader(sprites),
}
}
}
Expand All @@ -134,7 +111,7 @@ function createDynamicMatcher(
mode: string,
loaderOptions: IconifyLoaderOptions,
iconLoaderResolver: () => Promise<UniversalIconLoader>,
sprites?: CSSSVGSpritesOptions,
sprites?: CSSSVGSprites,
) {
return <DynamicMatcher>(async ([full, body, _mode = mode]) => {
let collection = ''
Expand All @@ -147,11 +124,7 @@ function createDynamicMatcher(
if (body.includes(':')) {
[collection, name] = body.split(':')
svg = sprites
? await iconLoader(collection, name, {
...loaderOptions,
usedProps,
customCollections: sprites.customCollections,
})
? await loadSvgFromSprite(collection, name, sprites, { ...loaderOptions, usedProps })
: await iconLoader(collection, name, { ...loaderOptions, usedProps })
}
else {
Expand All @@ -160,11 +133,7 @@ function createDynamicMatcher(
collection = parts.slice(0, i).join('-')
name = parts.slice(i).join('-')
svg = sprites
? await iconLoader(collection, name, {
...loaderOptions,
usedProps,
customCollections: sprites.customCollections,
})
? await loadSvgFromSprite(collection, name, sprites, { ...loaderOptions, usedProps })
: await iconLoader(collection, name, { ...loaderOptions, usedProps })
if (svg)
break
Expand All @@ -178,7 +147,6 @@ function createDynamicMatcher(
}

let url: string
// TODO: resolve base path
if (sprites)
url = `url("unocss-${collection}-sprite.svg#shapes-${name}-view")`
else
Expand Down Expand Up @@ -211,3 +179,112 @@ function createDynamicMatcher(
}
})
}

function createCSSSVGSpriteLoader(sprites?: CSSSVGSprites) {
return async (collection: string) => {
const collections = sprites?.sprites[collection]
if (!collections)
return undefined

return await createUint8ArraySprite(
collection,
createAsyncSpriteIconsFactory(collections),
sprites?.warn ?? false,
)
}
}

function createGenerateCSSSVGSprites(sprites?: CSSSVGSprites) {
return async function* () {
if (sprites) {
const warn = sprites.warn ?? false
for (const [collection, collections] of Object.entries(sprites.sprites)) {
yield <GeneratedSpriteData>{
name: collection,
asset: await createUint8ArraySprite(
collection,
createAsyncSpriteIconsFactory(collections),
warn,
),
}
}
}
}
}

async function customizeSpriteIcon(
collection: string,
icon: string,
options: IconifyLoaderOptions,
svg?: string,
) {
if (!svg)
return svg

const cleanupIdx = svg.indexOf('<svg')
if (cleanupIdx > 0)
svg = svg.slice(cleanupIdx)

const { transform } = options?.customizations ?? {}
svg
= typeof transform === 'function'
? await transform(svg, collection, icon)
: svg

if (!svg.startsWith('<svg')) {
console.warn(
`Custom icon "${icon}" in "${collection}" is not a valid SVG`,
)
return svg
}

return await mergeIconProps(
options?.customizations?.trimCustomSvg === true
? trimSVG(svg)
: svg,
collection,
icon,
options,
undefined,
)
}

async function loadSvgFromSprite(
collectionName: string,
icon: string,
sprites: CSSSVGSprites,
options: IconifyLoaderOptions,
) {
const collections = sprites.sprites[collectionName]
if (!collections)
return

const collectionsArray = Array.isArray(collections)
? collections
: [collections]

for (const collection of collectionsArray) {
if (Array.isArray(collection)) {
for (const spriteIcon of collection) {
if (spriteIcon.name === icon)
return await customizeSpriteIcon(collectionName, icon, options, spriteIcon.svg)
}
}
else if ('svg' in collection) {
if (collection.name === icon)
return await customizeSpriteIcon(collectionName, icon, options, collection.svg)
}
else if (typeof collection === 'function') {
for await (const spriteIcon of collection()) {
if (spriteIcon.name === icon)
return await customizeSpriteIcon(collectionName, icon, options, spriteIcon.svg)
}
}
else {
for await (const spriteIcon of collection[Symbol.asyncIterator]()) {
if (spriteIcon.name === icon)
return await customizeSpriteIcon(collectionName, icon, options, spriteIcon.svg)
}
}
}
}
Loading

0 comments on commit 22d1414

Please sign in to comment.