neft
Version:
Universal Platform
252 lines (206 loc) • 7.31 kB
text/coffeescript
'use strict'
fs = require 'fs'
pathUtils = require 'path'
yaml = require 'js-yaml'
mkdirp = require 'mkdirp'
try
sharpModulePath = pathUtils.join fs.realpathSync('./'), '/node_modules/sharp'
sharp = require sharpModulePath
utils = require 'src/utils'
log = require 'src/log'
assert = require 'src/assert'
Resources = require 'src/resources'
log = log.scope 'Resources', 'Parser'
IMAGE_FORMATS =
__proto__: null
png: true
jpg: true
jpeg: true
gif: true
DEFAULT_CONFIG =
resolutions: [1]
stack = null
bgStack = null
logShowed = false
isResourcesPath = (path) ->
/\/resources\.(?:json|yaml)$/.test path
toCamelCase = (str) ->
words = str.split ' '
r = words[0]
for i in [1...words.length] by 1
r += utils.capitalize words[i]
r
resolutionToString = (resolution) ->
if resolution is 1
''
else
'@' + (resolution+'').replace('.', 'p') + 'x'
supportImageResource = (path, rsc) ->
unless sharp
unless logShowed
log.warn "Install 'sharp' module for full image resources support; 'npm install sharp'"
logShowed = true
return
# get size
stats = fs.statSync path
mtime = new Date stats.mtime
resizeImage = (path, width, height, output, callback) ->
sharp(path).resize(width, height).toFile(output, callback)
stack.add (callback) ->
sharp(path).metadata (err, meta) ->
if err
return callback err
name = pathUtils.basename path
name = Resources.Resource.parseFileName name
nameResolution = name.resolution ? 1
width = Math.round meta.width / nameResolution
height = Math.round meta.height / nameResolution
rsc.width = width
rsc.height = height
for format in rsc.formats
for resolution in rsc.resolutions
resPath = rsc.paths[format][resolution].slice(1)
if nameResolution isnt resolution
resPath = "build/#{resPath}"
if not (exists = fs.existsSync(resPath)) or new Date(fs.statSync(resPath).mtime) < mtime
unless exists
mkdirp.sync pathUtils.dirname(resPath)
bgStack.add resizeImage, null, [path, width * resolution, height * resolution, resPath]
callback()
return
parseResourcesFolder = (path) ->
throw "Resources folder not implemented"
parseResourcesFile = (path, config) ->
assert.isString path
file = fs.readFileSync path, 'utf-8'
try
if pathUtils.extname(path) in ['.yaml', '.yml']
json = yaml.safeLoad file
else
json = JSON.parse file
catch err
log.error "Error in file '#{path}'"
throw err
getValue json, path, config
parseResourcesObject = (obj, dirPath, config) ->
assert.isPlainObject obj
if obj.resources?
config = utils.clone config
utils.merge config, obj
delete config.resources
else
obj = resources: obj
if dirPath.indexOf('.') isnt -1
dirPath = pathUtils.dirname dirPath
if Array.isArray(obj.resources)
parseResourcesArray obj.resources, dirPath, config
else
rscs = new Resources
for prop, val of obj.resources
rscs[prop] = getValue val, dirPath, config
rscs
parseResourcesArray = (arr, dirPath, config) ->
obj = utils.arrayToObject arr, (_, val) ->
name = val.file or val
if isResourcesPath(name)
pathUtils.dirname(name)
else
Resources.Resource.parseFileName(name).file
parseResourcesObject obj, dirPath, config
parseResourceFile = (path, config) ->
dirPath = pathUtils.dirname path
name = pathUtils.basename path
name = Resources.Resource.parseFileName(name)
name.resolution ?= 1
unless fs.existsSync(path)
msg = "File '#{path}' doesn't exist"
unless name.format
msg += "; format is missed"
log.error msg
return
rsc = new Resources.Resource
newConfig = {}
for key, val of config
newConfig[toCamelCase(key)] = utils.cloneDeep val
utils.merge rsc, newConfig
if newConfig.file
rsc.file = name.file
if rsc.resolutions
unless utils.has(rsc.resolutions, name.resolution)
rsc.resolutions.push name.resolution
else
rsc.resolutions = [name.resolution]
# remove greater resolutions
rsc.resolutions = rsc.resolutions.filter (elem) ->
elem <= name.resolution
if name.format
if rsc.formats
# log.warn "Multiple formats are not currently supported; '#{rsc.formats}' got"
unless utils.has(rsc.formats, name.format)
rsc.formats.push name.format
else
rsc.formats = [name.format]
rsc.formats ?= []
paths = rsc.paths = {}
for format in rsc.formats
formatPaths = paths[format] = {}
for resolution in rsc.resolutions
resPath = "/#{dirPath}/#{name.file}#{resolutionToString(resolution)}.#{format}"
formatPaths[resolution] = resPath
if IMAGE_FORMATS[name.format]
supportImageResource path, rsc
rsc
getValue = (val, dirPath, config) ->
if utils.isObject(val)
config = utils.clone config
utils.merge config, val
delete config.file
delete config.resources
if typeof val is 'string'
path = pathUtils.join dirPath, val
getFile path, config
else if val?.file
path = pathUtils.join dirPath, val.file
getFile path, config
else if Array.isArray(val)
parseResourcesArray val, dirPath, config
else if utils.isObject(val)
if val.resources?
if Array.isArray(val.resources)
parseResourcesArray val.resources, dirPath, config
else
parseResourcesObject val.resources, dirPath, config
else
config = utils.clone config
utils.merge config, val
parseResourceFile dirPath, config
getFile = (path, config) ->
unless fs.existsSync(path)
log.error "File '#{path}' doesn't exist"
return
stat = fs.statSync path
possiblePaths = [
pathUtils.join(path, './resources.json'),
pathUtils.join(path, './resources.yaml'),
pathUtils.join(path, './resources.yml'),
]
if isResourcesPath(path)
return parseResourcesFile path, config
if stat.isDirectory()
for possiblePath in possiblePaths
if fs.existsSync(possiblePath)
return parseResourcesFile possiblePath, config
return parseResourcesFolder path, config
return parseResourceFile path, config
exports.parse = (path, callback) ->
stack = new utils.async.Stack
bgStack = new utils.async.Stack
rscs = getFile path, DEFAULT_CONFIG
stack.runAllSimultaneously (err) ->
if not err and bgStack.length
logtime = log.time 'Resize images'
bgStack.runAllSimultaneously ->
log.end logtime
unless rscs instanceof Resources
rscs = {}
callback err, rscs