| 1 |
import { getSnippetType } from './snippets/snippets' |
| 2 |
import type { SnippetsExport } from '../types/schema/SnippetsExport' |
| 3 |
import type { Snippet } from '../types/Snippet' |
| 4 |
|
| 5 |
const SECOND_IN_MS = 1000 |
| 6 |
const TIMEOUT_SECONDS = 40 |
| 7 |
const JSON_INDENT_SPACES = 2 |
| 8 |
const EXPORT_FILENAME = 'snippets' |
| 9 |
const EXPORT_GENERATOR = 'Code Snippets' |
| 10 |
const DEFAULT_PRIORITY = 10 |
| 11 |
const EXPORT_DATE_LENGTH = 16 |
| 12 |
|
| 13 |
const MIME_INFO = <const> { |
| 14 |
php: ['php', 'text/php'], |
| 15 |
html: ['php', 'text/php'], |
| 16 |
css: ['css', 'text/css'], |
| 17 |
js: ['js', 'text/javascript'], |
| 18 |
cond: ['json', 'application/json'], |
| 19 |
json: ['json', 'application/json'] |
| 20 |
} |
| 21 |
|
| 22 |
export const downloadAsFile = (content: BlobPart, filename: string, type: string) => { |
| 23 |
const link = document.createElement('a') |
| 24 |
link.download = filename |
| 25 |
link.href = URL.createObjectURL(new Blob([content], { type })) |
| 26 |
|
| 27 |
setTimeout(() => URL.revokeObjectURL(link.href), TIMEOUT_SECONDS * SECOND_IN_MS) |
| 28 |
|
| 29 |
// Some browsers (notably headless Chromium) can ignore programmatic clicks on detached anchors. |
| 30 |
// Appending the link to the DOM before clicking improves reliability. |
| 31 |
link.style.display = 'none' |
| 32 |
document.body.appendChild(link) |
| 33 |
|
| 34 |
setTimeout(() => { |
| 35 |
link.click() |
| 36 |
link.remove() |
| 37 |
}, 0) |
| 38 |
} |
| 39 |
|
| 40 |
export const downloadSnippetExportFile = ( |
| 41 |
content: SnippetsExport | string, |
| 42 |
{ id, name, scope }: Snippet, |
| 43 |
type?: keyof typeof MIME_INFO |
| 44 |
) => { |
| 45 |
const sanitizedName = name.toLowerCase().replace(/[^\w-]+/g, '-').trim() |
| 46 |
const title = '' === sanitizedName ? `snippet-${id}` : sanitizedName |
| 47 |
|
| 48 |
if ('string' === typeof content) { |
| 49 |
const [ext, mimeType] = MIME_INFO[type ?? getSnippetType({ scope })] |
| 50 |
const filename = `${title}.code-snippets.${ext}` |
| 51 |
downloadAsFile(content, filename, mimeType) |
| 52 |
} else { |
| 53 |
const filename = `${title}.code-snippets.json` |
| 54 |
downloadAsFile(JSON.stringify(content, undefined, JSON_INDENT_SPACES), filename, 'application/json') |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
type DefaultExportValueCheck = (value: unknown) => boolean |
| 59 |
|
| 60 |
const isNullish = (value: unknown): value is undefined | null => undefined === value || null === value |
| 61 |
|
| 62 |
const DEFAULT_EXPORT_VALUE_CHECKS: Record<string, DefaultExportValueCheck> = { |
| 63 |
id: value => 0 === value, |
| 64 |
desc: value => '' === value, |
| 65 |
name: value => '' === value, |
| 66 |
code: value => '' === value, |
| 67 |
tags: value => Array.isArray(value) && 0 === value.length, |
| 68 |
scope: value => 'global' === value, |
| 69 |
condition_id: value => 0 === value, |
| 70 |
active: value => false === value, |
| 71 |
locked: value => false === value, |
| 72 |
trashed: value => false === value, |
| 73 |
priority: value => DEFAULT_PRIORITY === value, |
| 74 |
modified: value => '' === value || isNullish(value), |
| 75 |
last_active: value => 0 === value || isNullish(value), |
| 76 |
network: value => null === value || false === value, |
| 77 |
shared_network: value => null === value || false === value, |
| 78 |
code_error: value => null === value || false === value, |
| 79 |
code_error_trace: value => null === value || false === value |
| 80 |
} |
| 81 |
|
| 82 |
const isDefaultExportValue = (field: string, value: unknown): boolean => |
| 83 |
(DEFAULT_EXPORT_VALUE_CHECKS[field] ?? isNullish)(value) |
| 84 |
|
| 85 |
const buildExportSnippet = ({ |
| 86 |
id, |
| 87 |
name, |
| 88 |
desc, |
| 89 |
code, |
| 90 |
tags, |
| 91 |
scope, |
| 92 |
active, |
| 93 |
locked, |
| 94 |
trashed, |
| 95 |
network, |
| 96 |
shared_network, |
| 97 |
modified, |
| 98 |
priority, |
| 99 |
conditionId, |
| 100 |
lastActive, |
| 101 |
code_error, |
| 102 |
code_error_trace |
| 103 |
}: Snippet): SnippetsExport['snippets'][number] => |
| 104 |
Object.fromEntries( |
| 105 |
Object.entries({ |
| 106 |
id, |
| 107 |
name, |
| 108 |
desc, |
| 109 |
code, |
| 110 |
tags, |
| 111 |
scope, |
| 112 |
active, |
| 113 |
locked, |
| 114 |
trashed, |
| 115 |
network, |
| 116 |
shared_network, |
| 117 |
modified, |
| 118 |
priority, |
| 119 |
condition_id: conditionId, |
| 120 |
last_active: lastActive, |
| 121 |
code_error, |
| 122 |
code_error_trace |
| 123 |
}).filter(([field, value]) => !isDefaultExportValue(field, value)) |
| 124 |
) |
| 125 |
|
| 126 |
export const downloadBulkSnippetExportFile = (snippets: readonly Snippet[]) => { |
| 127 |
if (0 === snippets.length) { |
| 128 |
return |
| 129 |
} |
| 130 |
|
| 131 |
const content: SnippetsExport = { |
| 132 |
generator: EXPORT_GENERATOR, |
| 133 |
date_created: new Date().toISOString().slice(0, EXPORT_DATE_LENGTH).replace('T', ' '), |
| 134 |
snippets: snippets.map(buildExportSnippet) |
| 135 |
} |
| 136 |
|
| 137 |
downloadAsFile( |
| 138 |
JSON.stringify(content, undefined, JSON_INDENT_SPACES), |
| 139 |
`${EXPORT_FILENAME}.code-snippets.json`, |
| 140 |
'application/json' |
| 141 |
) |
| 142 |
} |
| 143 |
|