Initial commit: WordPress wp-content (themes, plugins, languages)
- Theme: momentry (custom theme with REST API routes) - Plugins: code-snippets (contains all API proxies) - Languages: zh_TW translations - Excludes: cache, backups, uploads, logs
This commit is contained in:
170
plugins/code-snippets/js/utils/Linter.ts
Normal file
170
plugins/code-snippets/js/utils/Linter.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Based on work licensed under the BSD 3-Clause license.
|
||||
*
|
||||
* Copyright (c) 2017, glayzzle
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import { Engine } from 'php-parser'
|
||||
import CodeMirror from 'codemirror'
|
||||
import type { Block, Location, Node } from 'php-parser'
|
||||
|
||||
export interface Annotation {
|
||||
message: string
|
||||
severity: string
|
||||
from: CodeMirror.Position
|
||||
to: CodeMirror.Position
|
||||
}
|
||||
|
||||
export interface Identifier extends Node {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Declaration extends Node {
|
||||
name: Identifier | string
|
||||
}
|
||||
|
||||
export class Linter {
|
||||
private readonly code: string
|
||||
|
||||
private readonly function_names: Set<string>
|
||||
|
||||
private readonly class_names: Set<string>
|
||||
|
||||
public readonly annotations: Annotation[]
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param code
|
||||
*/
|
||||
constructor(code: string) {
|
||||
this.code = code
|
||||
this.annotations = []
|
||||
|
||||
this.function_names = new Set()
|
||||
this.class_names = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lint the provided code.
|
||||
*/
|
||||
lint() {
|
||||
const parser = new Engine({
|
||||
parser: {
|
||||
suppressErrors: true,
|
||||
version: 800
|
||||
},
|
||||
ast: {
|
||||
withPositions: true
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const program = parser.parseEval(this.code)
|
||||
|
||||
if (0 < program.errors.length) {
|
||||
for (const error of program.errors) {
|
||||
this.annotate(error.message, error.loc)
|
||||
}
|
||||
}
|
||||
|
||||
this.visit(program)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodes recursively.
|
||||
* @param node
|
||||
*/
|
||||
visit(node: Node) {
|
||||
if (node.kind) {
|
||||
this.validate(node)
|
||||
}
|
||||
|
||||
if ('children' in node) {
|
||||
const block = <Block> node
|
||||
for (const child of block.children) {
|
||||
this.visit(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given identifier has already been defined, creating an annotation if so.
|
||||
* @param identifier
|
||||
* @param registry
|
||||
* @param label
|
||||
*/
|
||||
checkDuplicateIdentifier(identifier: Identifier, registry: Set<string>, label: string) {
|
||||
if (registry.has(identifier.name)) {
|
||||
this.annotate(`Cannot redeclare ${label} ${identifier.name}()`, identifier.loc)
|
||||
} else {
|
||||
registry.add(identifier.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform additional validations on nodes.
|
||||
* @param node
|
||||
*/
|
||||
validate(node: Node) {
|
||||
const decl = <Declaration> node
|
||||
const ident = <Identifier> decl.name
|
||||
|
||||
if (!('name' in decl && 'name' in ident) || 'identifier' !== ident.kind) {
|
||||
return
|
||||
}
|
||||
|
||||
if ('function' === node.kind) {
|
||||
this.checkDuplicateIdentifier(ident, this.function_names, 'function')
|
||||
} else if ('class' === node.kind) {
|
||||
this.checkDuplicateIdentifier(ident, this.class_names, 'class')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a lint annotation.
|
||||
* @param message
|
||||
* @param location
|
||||
* @param severity
|
||||
*/
|
||||
annotate(message: string, location: Location | null, severity = 'error') {
|
||||
const [start, end] = location
|
||||
? location.end.offset < location.start.offset ? [location.end, location.start] : [location.start, location.end]
|
||||
: [{ line: 0, column: 0 }, { line: 0, column: 0 }]
|
||||
|
||||
this.annotations.push({
|
||||
message,
|
||||
severity,
|
||||
from: CodeMirror.Pos(start.line - 1, start.column),
|
||||
to: CodeMirror.Pos(end.line - 1, end.column)
|
||||
})
|
||||
}
|
||||
}
|
||||
3
plugins/code-snippets/js/utils/errors.ts
Normal file
3
plugins/code-snippets/js/utils/errors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const handleUnknownError = (error: unknown) => {
|
||||
console.error(error)
|
||||
}
|
||||
43
plugins/code-snippets/js/utils/files.ts
Normal file
43
plugins/code-snippets/js/utils/files.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { getSnippetType } from './snippets/snippets'
|
||||
import type { SnippetsExport } from '../types/schema/SnippetsExport'
|
||||
import type { Snippet } from '../types/Snippet'
|
||||
|
||||
const SECOND_IN_MS = 1000
|
||||
const TIMEOUT_SECONDS = 40
|
||||
const JSON_INDENT_SPACES = 2
|
||||
|
||||
const MIME_INFO = <const> {
|
||||
php: ['php', 'text/php'],
|
||||
html: ['php', 'text/php'],
|
||||
css: ['css', 'text/css'],
|
||||
js: ['js', 'text/javascript'],
|
||||
cond: ['json', 'application/json'],
|
||||
json: ['json', 'application/json']
|
||||
}
|
||||
|
||||
export const downloadAsFile = (content: BlobPart, filename: string, type: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.download = filename
|
||||
link.href = URL.createObjectURL(new Blob([content], { type }))
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), TIMEOUT_SECONDS * SECOND_IN_MS)
|
||||
setTimeout(() => link.click(), 0)
|
||||
}
|
||||
|
||||
export const downloadSnippetExportFile = (
|
||||
content: SnippetsExport | string,
|
||||
{ id, name, scope }: Snippet,
|
||||
type?: keyof typeof MIME_INFO
|
||||
) => {
|
||||
const sanitizedName = name.toLowerCase().replace(/[^\w-]+/g, '-').trim()
|
||||
const title = '' === sanitizedName ? `snippet-${id}` : sanitizedName
|
||||
|
||||
if ('string' === typeof content) {
|
||||
const [ext, mimeType] = MIME_INFO[type ?? getSnippetType({ scope })]
|
||||
const filename = `${title}.code-snippets.${ext}`
|
||||
downloadAsFile(content, filename, mimeType)
|
||||
} else {
|
||||
const filename = `${title}.code-snippets.json`
|
||||
downloadAsFile(JSON.stringify(content, undefined, JSON_INDENT_SPACES), filename, 'application/json')
|
||||
}
|
||||
}
|
||||
21
plugins/code-snippets/js/utils/hooks.ts
Normal file
21
plugins/code-snippets/js/utils/hooks.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { Context } from 'react'
|
||||
|
||||
export const createContextHook = <T>(name: string): [
|
||||
Context<T | undefined>,
|
||||
() => T
|
||||
] => {
|
||||
const contextValue = createContext<T | undefined>(undefined)
|
||||
|
||||
const useContextHook = (): T => {
|
||||
const value = useContext(contextValue)
|
||||
|
||||
if (value === undefined) {
|
||||
throw Error(`use${name} can only be used within a ${name} context provider.`)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return [contextValue, useContextHook]
|
||||
}
|
||||
12
plugins/code-snippets/js/utils/restAPI.ts
Normal file
12
plugins/code-snippets/js/utils/restAPI.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { trimTrailingChar } from './text'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
export const REST_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.base ?? '', '/')
|
||||
export const REST_SNIPPETS_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.snippets ?? '', '/')
|
||||
|
||||
export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce,
|
||||
'Access-Control': window.CODE_SNIPPETS?.restAPI.localToken
|
||||
}
|
||||
}
|
||||
8
plugins/code-snippets/js/utils/screen.ts
Normal file
8
plugins/code-snippets/js/utils/screen.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const isNetworkAdmin = (): boolean =>
|
||||
window.pagenow.endsWith('-network')
|
||||
|
||||
export const isMacOS = (): boolean =>
|
||||
null !== /mac/i.exec(window.navigator.userAgent)
|
||||
|
||||
export const isLicensed = (): boolean =>
|
||||
!!window.CODE_SNIPPETS?.isLicensed
|
||||
92
plugins/code-snippets/js/utils/snippets/api.ts
Normal file
92
plugins/code-snippets/js/utils/snippets/api.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { addQueryArgs } from '@wordpress/url'
|
||||
import { REST_SNIPPETS_BASE } from '../restAPI'
|
||||
import { createSnippetObject } from './snippets'
|
||||
import type { RestAPI } from '../../hooks/useRestAPI'
|
||||
import type { SnippetSchema, WritableSnippetSchema } from '../../types/schema/SnippetSchema'
|
||||
import type { Snippet } from '../../types/Snippet'
|
||||
import type { SnippetsExport } from '../../types/schema/SnippetsExport'
|
||||
|
||||
export interface SnippetsAPI {
|
||||
fetchAll: (network?: boolean | null) => Promise<Snippet[]>
|
||||
fetch: (snippetId: number, network?: boolean | null) => Promise<Snippet>
|
||||
create: (snippet: Snippet) => Promise<Snippet>
|
||||
update: (snippet: Pick<Snippet, 'id' | 'network'> & Partial<Snippet>) => Promise<Snippet>
|
||||
delete: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<void>
|
||||
activate: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<Snippet>
|
||||
deactivate: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<Snippet>
|
||||
export: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<SnippetsExport>
|
||||
exportCode: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<string>
|
||||
attach: (snippet: Pick<Snippet, 'id' | 'network' | 'conditionId'>) => Promise<void>
|
||||
detach: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<void>
|
||||
}
|
||||
|
||||
const buildURL = ({ id, network }: Pick<Snippet, 'id' | 'network'>, action?: string) =>
|
||||
addQueryArgs(
|
||||
[REST_SNIPPETS_BASE, id, action].filter(Boolean).join('/'),
|
||||
{ network: network ? true : undefined }
|
||||
)
|
||||
|
||||
const mapToSchema = ({
|
||||
name,
|
||||
desc,
|
||||
code,
|
||||
tags,
|
||||
scope,
|
||||
priority,
|
||||
active,
|
||||
network,
|
||||
shared_network,
|
||||
conditionId
|
||||
}: Partial<Snippet>): WritableSnippetSchema => ({
|
||||
name,
|
||||
desc,
|
||||
code,
|
||||
tags,
|
||||
scope,
|
||||
priority,
|
||||
active,
|
||||
network,
|
||||
shared_network,
|
||||
condition_id: conditionId
|
||||
})
|
||||
|
||||
export const buildSnippetsAPI = ({ get, post, del, put }: RestAPI): SnippetsAPI => ({
|
||||
fetchAll: network =>
|
||||
get<SnippetSchema[]>(addQueryArgs(REST_SNIPPETS_BASE, { network }))
|
||||
.then(response => response.map(createSnippetObject)),
|
||||
|
||||
fetch: (snippetId, network) =>
|
||||
get<SnippetSchema>(addQueryArgs(`${REST_SNIPPETS_BASE}/${snippetId}`, { network }))
|
||||
.then(createSnippetObject),
|
||||
|
||||
create: snippet =>
|
||||
post<SnippetSchema>(REST_SNIPPETS_BASE, mapToSchema(snippet))
|
||||
.then(createSnippetObject),
|
||||
|
||||
update: snippet =>
|
||||
post<SnippetSchema>(snippet.id ? buildURL(snippet) : REST_SNIPPETS_BASE, mapToSchema(snippet))
|
||||
.then(createSnippetObject),
|
||||
|
||||
delete: snippet =>
|
||||
del(buildURL(snippet)),
|
||||
|
||||
activate: snippet =>
|
||||
post<SnippetSchema>(buildURL(snippet, 'activate'))
|
||||
.then(createSnippetObject),
|
||||
|
||||
deactivate: snippet =>
|
||||
post<SnippetSchema>(buildURL(snippet, 'deactivate'))
|
||||
.then(createSnippetObject),
|
||||
|
||||
export: snippet =>
|
||||
get<SnippetsExport>(buildURL(snippet, 'export')),
|
||||
|
||||
exportCode: snippet =>
|
||||
get<string>(buildURL(snippet, 'export-code')),
|
||||
|
||||
attach: snippet =>
|
||||
put(buildURL(snippet, 'attach'), { condition_id: snippet.conditionId }),
|
||||
|
||||
detach: snippet =>
|
||||
put(buildURL(snippet, 'detach'))
|
||||
})
|
||||
51
plugins/code-snippets/js/utils/snippets/objects.ts
Normal file
51
plugins/code-snippets/js/utils/snippets/objects.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet'
|
||||
import { isNetworkAdmin } from '../screen'
|
||||
import type { Snippet, SnippetScope } from '../../types/Snippet'
|
||||
|
||||
const defaults: Omit<Snippet, 'tags'> = {
|
||||
id: 0,
|
||||
name: '',
|
||||
code: '',
|
||||
desc: '',
|
||||
scope: 'global',
|
||||
modified: '',
|
||||
active: false,
|
||||
network: isNetworkAdmin(),
|
||||
shared_network: null,
|
||||
priority: 10,
|
||||
conditionId: 0
|
||||
}
|
||||
|
||||
const isAbsInt = (value: unknown): value is number =>
|
||||
'number' === typeof value && 0 < value
|
||||
|
||||
const parseStringArray = (value: unknown): string[] | undefined =>
|
||||
Array.isArray(value) ? value.filter(entry => 'string' === typeof entry) : undefined
|
||||
|
||||
export const isValidScope = (scope: unknown): scope is SnippetScope =>
|
||||
'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes =>
|
||||
typeScopes.some(typeScope => typeScope === scope))
|
||||
|
||||
export const parseSnippetObject = (fields: unknown): Snippet => {
|
||||
const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { ...defaults, tags: [] }
|
||||
|
||||
if ('object' !== typeof fields || null === fields) {
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
...'id' in fields && isAbsInt(fields.id) && { id: fields.id },
|
||||
...'name' in fields && 'string' === typeof fields.name && { name: fields.name },
|
||||
...'desc' in fields && 'string' === typeof fields.desc && { desc: fields.desc },
|
||||
...'code' in fields && 'string' === typeof fields.code && { code: fields.code },
|
||||
...'tags' in fields && { tags: parseStringArray(fields.tags) ?? result.tags },
|
||||
...'scope' in fields && isValidScope(fields.scope) && { scope: fields.scope },
|
||||
...'modified' in fields && 'string' === typeof fields.modified && { modified: fields.modified },
|
||||
...'active' in fields && 'boolean' === typeof fields.active && { active: fields.active },
|
||||
...'network' in fields && 'boolean' === typeof fields.network && { network: fields.network },
|
||||
...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network },
|
||||
...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority },
|
||||
...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }
|
||||
}
|
||||
}
|
||||
55
plugins/code-snippets/js/utils/snippets/snippets.ts
Normal file
55
plugins/code-snippets/js/utils/snippets/snippets.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { parseSnippetObject } from './objects'
|
||||
import type { Snippet, SnippetType } from '../../types/Snippet'
|
||||
|
||||
const PRO_TYPES = new Set<SnippetType>(['css', 'js', 'cond'])
|
||||
|
||||
export const createSnippetObject = (fields: unknown): Snippet =>
|
||||
parseSnippetObject(fields)
|
||||
|
||||
export const getSnippetType = ({ scope }: Pick<Snippet, 'scope'>): SnippetType => {
|
||||
switch (true) {
|
||||
case scope.endsWith('-css'):
|
||||
return 'css'
|
||||
|
||||
case scope.endsWith('-js'):
|
||||
return 'js'
|
||||
|
||||
case scope.endsWith('content'):
|
||||
return 'html'
|
||||
|
||||
case 'condition' === scope:
|
||||
return 'cond'
|
||||
|
||||
default:
|
||||
return 'php'
|
||||
}
|
||||
}
|
||||
|
||||
export const validateSnippet = (snippet: Snippet): undefined | string => {
|
||||
const missingTitle = '' === snippet.name.trim()
|
||||
const missingCode = '' === snippet.code.trim()
|
||||
|
||||
switch (true) {
|
||||
case missingCode && missingTitle:
|
||||
return __('This snippet has no code or title.', 'code-snippets')
|
||||
|
||||
case missingCode:
|
||||
return __('This snippet has no snippet code.', 'code-snippets')
|
||||
|
||||
case missingTitle:
|
||||
return __('This snippet has no title.', 'code-snippets')
|
||||
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const isCondition = (snippet: Pick<Snippet, 'scope'>): boolean =>
|
||||
'condition' === snippet.scope
|
||||
|
||||
export const isProSnippet = (snippet: Pick<Snippet, 'scope'>): boolean =>
|
||||
PRO_TYPES.has(getSnippetType(snippet))
|
||||
|
||||
export const isProType = (type: SnippetType): boolean =>
|
||||
PRO_TYPES.has(type)
|
||||
21
plugins/code-snippets/js/utils/text.ts
Normal file
21
plugins/code-snippets/js/utils/text.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const toCamelCase = (text: string): string =>
|
||||
text.replace(/-(?<letter>[a-z])/g, (_, letter: string) => letter.toUpperCase())
|
||||
|
||||
export const trimLeadingChar = (text: string, character: string): string =>
|
||||
character === text.charAt(0) ? text.slice(1) : text
|
||||
|
||||
export const trimTrailingChar = (text: string, character: string): string =>
|
||||
character === text.charAt(text.length - 1) ? text.slice(0, -1) : text
|
||||
|
||||
export const truncateWords = (text: string, wordCount: number): string => {
|
||||
const words = text.trim().split(/\s+/)
|
||||
|
||||
return words.length > wordCount
|
||||
? `${words.slice(0, wordCount).join(' ')}…`
|
||||
: text
|
||||
}
|
||||
|
||||
export const stripTags = (text: string): string =>
|
||||
text
|
||||
.replace(/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi, '')
|
||||
.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, '')
|
||||
Reference in New Issue
Block a user