diff --git a/haxelib.json b/haxelib.json index 08eff7b..04c4492 100755 --- a/haxelib.json +++ b/haxelib.json @@ -8,14 +8,13 @@ "template" ], "description": "View framework.", - "version": "1.1.0", + "version": "1.2.0", "releasenote": "Update.", "contributors": [ "shmyga" ], "classPath": "src/main", "dependencies": { - "promhx": "1.1.0", - "yaml": "1.3.0" + "promhx": "1.1.0" } } diff --git a/src/main/haxework/log/BaseLogger.hx b/src/main/haxework/log/BaseLogger.hx index 79ce3d1..02a52b0 100755 --- a/src/main/haxework/log/BaseLogger.hx +++ b/src/main/haxework/log/BaseLogger.hx @@ -12,11 +12,11 @@ class LoggerUtil { public static function printStackItem(item:StackItem):String { return switch item { - case StackItem.CFunction: 'CFunction'; - case StackItem.Module(m): m; - case StackItem.FilePos(s, file, line): '${file}:${line}'; - case StackItem.Method(classname, method): '${classname}::${method}}'; - case StackItem.LocalFunction(v): 'LocalFunction(${v})'; + case CFunction: 'CFunction'; + case Module(m): m; + case FilePos(s, file, line): '${file}:${line}'; + case Method(classname, method): '${classname}::${method}}'; + case LocalFunction(v): 'LocalFunction(${v})'; } } @@ -55,4 +55,4 @@ class BaseLogger implements ILogger { public function e(tag:String, message:String, ?error:Dynamic, ?p:haxe.PosInfos):Void { log(LogLevel.ERROR, tag, message, error, p); } -} \ No newline at end of file +} diff --git a/src/main/haxework/log/TraceLogger.hx b/src/main/haxework/log/TraceLogger.hx index fb5c125..82988d4 100755 --- a/src/main/haxework/log/TraceLogger.hx +++ b/src/main/haxework/log/TraceLogger.hx @@ -27,7 +27,9 @@ class TraceLogger extends BaseLogger { $print("\n"); } #elseif js - untyped js.Boot.__trace(v, infos); + if (js.Syntax.typeof(untyped console) != "undefined" && (untyped console).log != null) + (untyped console).log(v); + //untyped js.Boot.__trace(v, infos); #elseif android haxework.log.AndroidLog.write(3, "", ConstCharStar.fromString(Std.string(v))); #elseif (php && php7) diff --git a/src/main/yaml/Parser.hx b/src/main/yaml/Parser.hx new file mode 100644 index 0000000..cb3cf21 --- /dev/null +++ b/src/main/yaml/Parser.hx @@ -0,0 +1,2017 @@ +package yaml; + +import Type; +import yaml.util.ObjectMap; +import yaml.util.StringMap; +import yaml.util.IntMap; +import haxe.Utf8; +import haxe.PosInfos; +import yaml.schema.DefaultSchema; +import yaml.schema.SafeSchema; +import yaml.YamlType; +import yaml.util.Strings; +import yaml.util.Ints; + +class ParserOptions +{ + public var schema:Schema; + public var resolve:Bool; + public var validation:Bool; + public var strict:Bool; + public var maps:Bool; + + /** + @param schema Defines the schema to use while parsing. Defaults to yaml.schema.DefaultSchema. + */ + public function new(?schema:Schema = null) + { + this.schema = (schema == null) ? new DefaultSchema() : schema; + + strict = false; + resolve = true; + validation = true; + maps = true; + } + + /** + Use yaml.util.ObjectMap as the key => value container. + + Allows for complex key values. + */ + public function useMaps():ParserOptions + { + maps = true; + return this; + } + + /** + Use Dynamic objects as the key => value container. + + All keys will become Strings. + */ + public function useObjects():ParserOptions + { + maps = false; + return this; + } + + /** + Defines the schema to use while parsing. + + See yaml.Schema + */ + public function setSchema(schema:Schema):ParserOptions + { + this.schema = schema; + return this; + } + + /** + Warnings will be thrown as exceptions instead of traced. + */ + public function strictMode(?value:Bool = true):ParserOptions + { + strict = value; + return this; + } + + /** + Disables validation of yaml document while parsing. + */ + public function validate(?value:Bool = true):ParserOptions + { + validation = value; + return this; + } +} + +class Parser +{ + /** + Utility method to create ParserOptions for configuring a Parser instance. + */ + public static function options():ParserOptions + { + return new ParserOptions(); + } + + var schema:Schema; + var resolve:Bool; + var validate:Bool; + var strict:Bool; + var usingMaps:Bool; + + var directiveHandlers:StringMapArray->Void>; + var implicitTypes:Array; + var typeMap:StringMap; + + var length:Int; + var position:Int; + var line:Int; + var lineStart:Int; + var lineIndent:Int; + var character:Null; + + var version:String; + var checkLineBreaks:Bool; + var tagMap:StringMap; + var anchorMap:StringMap; + var tag:String; + var anchor:String; + var kind:String; + var result:Dynamic; + var input:String; + var output:Dynamic->Void; + + public function new() + {} + + public function safeParseAll(input:String, output:Dynamic->Void, options:ParserOptions):Void + { + options.schema = new SafeSchema(); + parseAll(input, output, options); + } + + public function safeParse(input:String, options:ParserOptions) + { + options.schema = new SafeSchema(); + return parse(input, options); + } + + public function parse(input:String, options:ParserOptions):Dynamic + { + var result:Dynamic = null; + var received = false; + + var responder = function (data:Dynamic) + { + if (!received) + { + result = data; + received = true; + } + else + { + throw new YamlException('expected a single document in the stream, but found more'); + } + } + + parseAll(input, responder, options); + + return result; + } + + public function parseAll(input:String, output:Dynamic->Void, options:ParserOptions):Void + { + #if (neko || cpp) + this.input = Utf8.encode(input); + #else + this.input = input; + #end + + this.output = output; + + schema = options.schema; + resolve = options.resolve; + validate = options.validation; + strict = options.strict; + usingMaps = options.maps; + + directiveHandlers = new StringMap(); + implicitTypes = schema.compiledImplicit; + typeMap = schema.compiledTypeMap; + + length = Utf8.length(this.input);//.length; + position = 0; + line = 0; + lineStart = 0; + lineIndent = 0; + character = Utf8.charCodeAt(this.input, position); + + directiveHandlers.set('YAML', function(name:String, args:Array) + { + #if (cpp || neko) + for (i in 0...args.length) + args[i] = Utf8.encode(args[i]); + #end + + if (null != version) + throwError('duplication of %YAML directive'); + + if (1 != args.length) + throwError('YAML directive accepts exactly one argument'); + + // match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); + var regex = ~/^([0-9]+)\.([0-9]+)$/u; + if (!regex.match(args[0])) + throwError('ill-formed argument of the YAML directive'); + + var major = Ints.parseInt(regex.matched(1), 10); + var minor = Ints.parseInt(regex.matched(2), 10); + + if (1 != major) + throwError('unacceptable YAML version of the document'); + + version = args[0]; + checkLineBreaks = (minor < 2); + + if (1 != minor && 2 != minor) + throwWarning('unsupported YAML version of the document'); + }); + + directiveHandlers.set('TAG', function(name:String, args:Array) + { + #if (cpp || neko) + for (i in 0...args.length) + args[i] = Utf8.encode(args[i]); + #end + + var handle:String; + var prefix:String; + + if (2 != args.length) + throwError('TAG directive accepts exactly two arguments'); + + handle = args[0]; + prefix = args[1]; + + if (!PATTERN_TAG_HANDLE.match(handle)) + throwError('ill-formed tag handle (first argument) of the TAG directive'); + + if (tagMap.exists(handle)) + throwError('there is a previously declared suffix for "' + handle + '" tag handle'); + + if (!PATTERN_TAG_URI.match(prefix)) + throwError('ill-formed tag prefix (second argument) of the TAG directive'); + + tagMap.set(handle, prefix); + }); + + if (validate && PATTERN_NON_PRINTABLE.match(this.input)) + { + throwError('the stream contains non-printable characters'); + } + + while (CHAR_SPACE == character) + { + lineIndent += 1; + character = Utf8.charCodeAt(input, ++position); + } + + while (position < length) + { + readDocument(); + } + } + + function generateError(message:String, ?info:PosInfos) + { + return new YamlException(message, info); + } + + function throwError(message:String, ?info:PosInfos) + { + throw generateError(message, info); + } + + function throwWarning(message:String, ?info:PosInfos) + { + var error = generateError(message, info); + + if (strict) { + throw error; + } else { + trace("Warning : " + error.toString()); + } + } + + function captureSegment(start:Int, end:Int, checkJson:Bool) + { + var _result:String; + + if (start < end) + { + _result = yaml.util.Utf8.substring(input, start, end); + + if (checkJson && validate) + { + for (pos in 0...Utf8.length(_result))//.length) + { + var char = Utf8.charCodeAt(_result, pos); + if (!(0x09 == char || 0x20 <= char && char <= 0x10FFFF)) + throwError('expected valid JSON character'); + } + } + + result += _result; + } + } + + // when create dynamic object graph + function mergeObjectMappings(destination:Dynamic, source:Dynamic) + { + if (Type.typeof(source) != ValueType.TObject) { + throwError('cannot merge mappings; the provided source object is unacceptable'); + } + + for (key in Reflect.fields(source)) + if (!Reflect.hasField(destination, key)) + Reflect.setField(destination, key, Reflect.field(source, key)); + } + + // when creating map based graph + function mergeMappings(destination:AnyObjectMap, source:AnyObjectMap) + { + if (!Std.is(source, AnyObjectMap)) { + throwError('cannot merge mappings; the provided source object is unacceptable'); + } + + for (key in source.keys()) + if (!destination.exists(key)) + destination.set(key, source.get(key)); + } + + + function storeObjectMappingPair(_result:Dynamic, keyTag:String, keyNode:Dynamic, valueNode:Dynamic):Dynamic + { + if (null == _result) + _result = {}; + + if ('tag:yaml.org,2002:merge' == keyTag) + { + if (Std.is(valueNode, Array)) + { + var list:Array = cast valueNode; + for (member in list) + mergeObjectMappings(_result, member); + } + else + { + mergeObjectMappings(_result, valueNode); + } + } + else + { + Reflect.setField(_result, Std.string(keyNode), valueNode); + } + return _result; + } + + function storeMappingPair(_result:AnyObjectMap, keyTag:String, keyNode:Dynamic, valueNode:Dynamic):AnyObjectMap + { + if (null == _result) + { + _result = new AnyObjectMap(); + } + + if ('tag:yaml.org,2002:merge' == keyTag) + { + if (Std.is(valueNode, Array)) + { + var list:Array = cast valueNode; + for (member in list) + mergeMappings(_result, member); + } + else + { + mergeMappings(_result, valueNode); + } + } + else + { + _result.set(keyNode, valueNode); + } + return _result; + } + + function readLineBreak() + { + if (CHAR_LINE_FEED == character) + { + position += 1; + } + else if (CHAR_CARRIAGE_RETURN == character) + { + if (CHAR_LINE_FEED == Utf8.charCodeAt(input, (position + 1))) + { + position += 2; + } + else + { + position += 1; + } + } + else + { + throwError('a line break is expected'); + } + + line += 1; + lineStart = position; + + if (position < length) + character = Utf8.charCodeAt(input, position); + else + character = null; + } + + function skipSeparationSpace(allowComments:Bool, checkIndent:Int) + { + var lineBreaks = 0; + + while (position < length) + { + while (CHAR_SPACE == character || CHAR_TAB == character) + { + character = Utf8.charCodeAt(input, ++position); + } + + if (allowComments && CHAR_SHARP == character) + { + do { character = Utf8.charCodeAt(input, ++position); } + while (position < length && CHAR_LINE_FEED != character && CHAR_CARRIAGE_RETURN != character); + } + + if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + readLineBreak(); + lineBreaks += 1; + lineIndent = 0; + + while (CHAR_SPACE == character) + { + lineIndent += 1; + character = Utf8.charCodeAt(input, ++position); + } + + if (lineIndent < checkIndent) + { + throwWarning('deficient indentation'); + } + } + else + { + break; + } + } + + return lineBreaks; + } + + function testDocumentSeparator() + { + if (position == lineStart && + (CHAR_MINUS == character || CHAR_DOT == character) && + Utf8.charCodeAt(input, (position + 1)) == character && + Utf8.charCodeAt(input, (position + 2)) == character) + { + + var pos = position + 3; + var char = Utf8.charCodeAt(input, pos); + + if (pos >= length || CHAR_SPACE == char || CHAR_TAB == char || + CHAR_LINE_FEED == char || CHAR_CARRIAGE_RETURN == char) + { + return true; + } + } + + return false; + } + + function writeFoldedLines(count:Int) + { + if (1 == count) + { + result += ' '; + } + else if (count > 1) + { + #if (cpp || neko) + result += Utf8.encode(Strings.repeat('\n', count - 1)); + #else + result += Strings.repeat('\n', count - 1); + #end + } + } + + function readPlainScalar(nodeIndent:Int, withinFlowCollection:Bool) + { + var preceding:Int; + var following:Int; + var captureStart:Int; + var captureEnd:Int; + var hasPendingContent; + + var _line:Int = 0; + var _kind = kind; + var _result = result; + + if (CHAR_SPACE == character || + CHAR_TAB == character || + CHAR_LINE_FEED == character || + CHAR_CARRIAGE_RETURN == character || + CHAR_COMMA == character || + CHAR_LEFT_SQUARE_BRACKET == character || + CHAR_RIGHT_SQUARE_BRACKET == character || + CHAR_LEFT_CURLY_BRACKET == character || + CHAR_RIGHT_CURLY_BRACKET == character || + CHAR_SHARP == character || + CHAR_AMPERSAND == character || + CHAR_ASTERISK == character || + CHAR_EXCLAMATION == character || + CHAR_VERTICAL_LINE == character || + CHAR_GREATER_THAN == character || + CHAR_SINGLE_QUOTE == character || + CHAR_DOUBLE_QUOTE == character || + CHAR_PERCENT == character || + CHAR_COMMERCIAL_AT == character || + CHAR_GRAVE_ACCENT == character) + { + return false; + } + + if (CHAR_QUESTION == character || CHAR_MINUS == character) + { + following = Utf8.charCodeAt(input, position + 1); + + if (CHAR_SPACE == following || + CHAR_TAB == following || + CHAR_LINE_FEED == following || + CHAR_CARRIAGE_RETURN == following || + withinFlowCollection && + (CHAR_COMMA == following || + CHAR_LEFT_SQUARE_BRACKET == following || + CHAR_RIGHT_SQUARE_BRACKET == following || + CHAR_LEFT_CURLY_BRACKET == following || + CHAR_RIGHT_CURLY_BRACKET == following)) + { + return false; + } + } + + kind = KIND_STRING; + result = ''; + captureStart = captureEnd = position; + hasPendingContent = false; + + while (position < length) + { + if (CHAR_COLON == character) + { + following = Utf8.charCodeAt(input, position + 1); + + if (CHAR_SPACE == following || + CHAR_TAB == following || + CHAR_LINE_FEED == following || + CHAR_CARRIAGE_RETURN == following || + withinFlowCollection && + (CHAR_COMMA == following || + CHAR_LEFT_SQUARE_BRACKET == following || + CHAR_RIGHT_SQUARE_BRACKET == following || + CHAR_LEFT_CURLY_BRACKET == following || + CHAR_RIGHT_CURLY_BRACKET == following)) + { + break; + } + + } + else if (CHAR_SHARP == character) + { + preceding = Utf8.charCodeAt(input, position - 1); + + if (CHAR_SPACE == preceding || + CHAR_TAB == preceding || + CHAR_LINE_FEED == preceding || + CHAR_CARRIAGE_RETURN == preceding) + { + break; + } + + } + else if ((position == lineStart && testDocumentSeparator()) || + withinFlowCollection && + (CHAR_COMMA == character || + CHAR_LEFT_SQUARE_BRACKET == character || + CHAR_RIGHT_SQUARE_BRACKET == character || + CHAR_LEFT_CURLY_BRACKET == character || + CHAR_RIGHT_CURLY_BRACKET == character)) + { + break; + + } + else if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + _line = line; + var _lineStart = lineStart; + var _lineIndent = lineIndent; + + skipSeparationSpace(false, -1); + + if (lineIndent >= nodeIndent) + { + hasPendingContent = true; + continue; + } + else + { + position = captureEnd; + line = _line; + lineStart = _lineStart; + lineIndent = _lineIndent; + character = Utf8.charCodeAt(input, position); + break; + } + } + + if (hasPendingContent) + { + captureSegment(captureStart, captureEnd, false); + writeFoldedLines(line - _line); + captureStart = captureEnd = position; + hasPendingContent = false; + } + + if (CHAR_SPACE != character && CHAR_TAB != character) + { + captureEnd = position + 1; + } + + if (++position >= length) + break; + + character = Utf8.charCodeAt(input, position); + } + + captureSegment(captureStart, captureEnd, false); + + if (result != null) + { + #if sys + result = Utf8.decode(result); // convert back into native encoding + #end + return true; + } + else + { + kind = _kind; + result = _result; + return false; + } + } + + function readSingleQuotedScalar(nodeIndent:Int) + { + var captureStart:Int; + var captureEnd:Int; + + if (CHAR_SINGLE_QUOTE != character) + { + return false; + } + + kind = KIND_STRING; + result = ''; + character = Utf8.charCodeAt(input, ++position); + captureStart = captureEnd = position; + + while (position < length) + { + if (CHAR_SINGLE_QUOTE == character) + { + captureSegment(captureStart, position, true); + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_SINGLE_QUOTE == character) + { + captureStart = captureEnd = position; + character = Utf8.charCodeAt(input, ++position); + } + else + { + #if sys + result = Utf8.decode(result); + #end + return true; + } + + } + else if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + captureSegment(captureStart, captureEnd, true); + writeFoldedLines(skipSeparationSpace(false, nodeIndent)); + captureStart = captureEnd = position; + character = Utf8.charCodeAt(input, position); + } + else if (position == lineStart && testDocumentSeparator()) + { + throwError('unexpected end of the document within a single quoted scalar'); + } + else + { + character = Utf8.charCodeAt(input, ++position); + captureEnd = position; + } + } + + throwError('unexpected end of the stream within a single quoted scalar'); + return false; + } + + function readDoubleQuotedScalar(nodeIndent:Int) + { + var captureStart:Int; + var captureEnd:Int; + + if (CHAR_DOUBLE_QUOTE != character) + return false; + + kind = KIND_STRING; + result = ''; + character = Utf8.charCodeAt(input, ++position); + captureStart = captureEnd = position; + + while (position < length) + { + if (CHAR_DOUBLE_QUOTE == character) + { + captureSegment(captureStart, position, true); + character = Utf8.charCodeAt(input, ++position); + + #if sys + result = Utf8.decode(result); + #end + return true; + + } + else if (CHAR_BACKSLASH == character) + { + captureSegment(captureStart, position, true); + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + skipSeparationSpace(false, nodeIndent); + } + else if (SIMPLE_ESCAPE_SEQUENCES.exists(character)) + { + result += SIMPLE_ESCAPE_SEQUENCES.get(character); + character = Utf8.charCodeAt(input, ++position); + } + else if (HEXADECIMAL_ESCAPE_SEQUENCES.exists(character)) + { + var hexLength = HEXADECIMAL_ESCAPE_SEQUENCES.get(character); + var hexResult = 0; + + for (hexIndex in 1...hexLength) + { + var hexOffset = (hexLength - hexIndex) * 4; + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_DIGIT_ZERO <= character && character <= CHAR_DIGIT_NINE) + { + hexResult |= (character - CHAR_DIGIT_ZERO) << hexOffset; + } + else if (CHAR_CAPITAL_A <= character && character <= CHAR_CAPITAL_F) + { + hexResult |= (character - CHAR_CAPITAL_A + 10) << hexOffset; + } + else if (CHAR_SMALL_A <= character && character <= CHAR_SMALL_F) + { + hexResult |= (character - CHAR_SMALL_A + 10) << hexOffset; + } + else { + throwError('expected hexadecimal character'); + } + } + + result += String.fromCharCode(hexResult); + character = Utf8.charCodeAt(input, ++position); + + } + else + { + throwError('unknown escape sequence'); + } + + captureStart = captureEnd = position; + + } + else if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + captureSegment(captureStart, captureEnd, true); + writeFoldedLines(skipSeparationSpace(false, nodeIndent)); + captureStart = captureEnd = position; + character = Utf8.charCodeAt(input, position); + } + else if (position == lineStart && testDocumentSeparator()) + { + throwError('unexpected end of the document within a double quoted scalar'); + } + else + { + character = Utf8.charCodeAt(input, ++position); + captureEnd = position; + } + } + + throwError('unexpected end of the stream within a double quoted scalar'); + return false; + } + + function composeNode(parentIndent:Int, nodeContext:Int, allowToSeek:Bool, allowCompact:Bool) + { + var allowBlockStyles:Bool; + var allowBlockScalars:Bool; + var allowBlockCollections:Bool; + var atNewLine = false; + var isIndented = true; + var hasContent = false; + + tag = null; + anchor = null; + kind = null; + result = null; + + allowBlockCollections = (CONTEXT_BLOCK_OUT == nodeContext || CONTEXT_BLOCK_IN == nodeContext); + allowBlockStyles = allowBlockScalars = allowBlockCollections; + + if (allowToSeek) + { + if (skipSeparationSpace(true, -1) != 0) + { + atNewLine = true; + + if (lineIndent == parentIndent) + { + isIndented = false; + } + else if (lineIndent > parentIndent) + { + isIndented = true; + } + else + { + return false; + } + } + } + + if (isIndented) + { + while (readTagProperty() || readAnchorProperty()) + { + if (skipSeparationSpace(true, -1) != 0) + { + atNewLine = true; + + if (lineIndent > parentIndent) + { + isIndented = true; + allowBlockCollections = allowBlockStyles; + } + else if (lineIndent == parentIndent) + { + isIndented = false; + allowBlockCollections = allowBlockStyles; + + } + else + { + return true; + } + } + else + { + allowBlockCollections = false; + } + } + } + + if (allowBlockCollections) + allowBlockCollections = atNewLine || allowCompact; + + if (isIndented || CONTEXT_BLOCK_OUT == nodeContext) + { + var flowIndent:Int; + var blockIndent:Int; + if (CONTEXT_FLOW_IN == nodeContext || CONTEXT_FLOW_OUT == nodeContext) + { + flowIndent = parentIndent; + } + else + { + flowIndent = parentIndent + 1; + } + + blockIndent = position - lineStart; + + if (isIndented) + { + if (allowBlockCollections && + (readBlockSequence(blockIndent) || + readBlockMapping(blockIndent)) || + readFlowCollection(flowIndent)) + { + hasContent = true; + } + else + { + if ((allowBlockScalars && readBlockScalar(flowIndent)) || + readSingleQuotedScalar(flowIndent) || + readDoubleQuotedScalar(flowIndent)) + { + hasContent = true; + } + else if (readAlias()) + { + hasContent = true; + + if (null != tag || null != anchor) + { + throwError('alias node should not have any properties'); + } + + } + else if (readPlainScalar(flowIndent, CONTEXT_FLOW_IN == nodeContext)) + { + hasContent = true; + + if (null == tag) + tag = '?'; + } + + + if (null != anchor) + { + anchorMap.set(anchor, result); + } + } + } + else + { + hasContent = allowBlockCollections && readBlockSequence(blockIndent); + } + } + + if (null != tag && '!' != tag) + { + var _result:Dynamic = null; + + if ('?' == tag) + { + if (resolve) + { + for (typeIndex in 0...implicitTypes.length) + { + var type = implicitTypes[typeIndex]; + // Implicit resolving is not allowed for non-scalar types, and '?' + // non-specific tag is only assigned to plain scalars. So, it isn't + // needed to check for 'kind' conformity. + var resolvedType = false; + + try + { + _result = type.resolve(result, usingMaps, false); + #if sys + if (Std.is(_result, String)) + _result = Utf8.decode(_result); + #end + tag = type.tag; + result = _result; + resolvedType = true; + } + catch (e:ResolveTypeException) {} + + if (resolvedType) break; + } + } + } + else if (typeMap.exists(tag)) + { + var t = typeMap.get(tag); + + if (null != result && t.loader.kind != kind) + { + throwError('unacceptable node kind for !<' + tag + '> tag; it should be "' + t.loader.kind + '", not "' + kind + '"'); + } + + if (!t.loader.skip) + { + + try + { + _result = t.resolve(result, usingMaps, true); + #if sys + if (Std.is(_result, String)) + _result = Utf8.decode(_result); + #end + + result = _result; + } + catch(e:ResolveTypeException) + { + throwError('cannot resolve a node with !<' + tag + '> explicit tag'); + } + } + } + else + { + throwWarning('unknown tag !<' + tag + '>'); + } + } + + return (null != tag || null != anchor || hasContent); + } + + function readFlowCollection(nodeIndent:Int) + { + var readNext = true; + var _tag = tag; + var _result:Dynamic; + var terminator:Int; + var isPair:Bool; + var isExplicitPair:Bool; + var isMapping:Bool; + var keyNode:Dynamic; + var keyTag:String; + var valueNode:Dynamic; + + switch (character) + { + case CHAR_LEFT_SQUARE_BRACKET: + terminator = CHAR_RIGHT_SQUARE_BRACKET; + isMapping = false; + _result = []; + + case CHAR_LEFT_CURLY_BRACKET: + terminator = CHAR_RIGHT_CURLY_BRACKET; + isMapping = true; + _result = usingMaps ? new ObjectMap<{}, Dynamic>() : {}; + + default: + return false; + } + + if (null != anchor) + anchorMap.set(anchor, _result); + + character = Utf8.charCodeAt(input, ++position); + + while (position < length) + { + skipSeparationSpace(true, nodeIndent); + + if (character == terminator) + { + character = Utf8.charCodeAt(input, ++position); + tag = _tag; + kind = isMapping ? KIND_OBJECT : KIND_ARRAY; + result = _result; + return true; + } + else if (!readNext) + { + throwError('missed comma between flow collection entries'); + } + + keyTag = keyNode = valueNode = null; + isPair = isExplicitPair = false; + + if (CHAR_QUESTION == character) + { + var following = Utf8.charCodeAt(input, position + 1); + + if (CHAR_SPACE == following || + CHAR_TAB == following || + CHAR_LINE_FEED == following || + CHAR_CARRIAGE_RETURN == following) + { + isPair = isExplicitPair = true; + position += 1; + character = following; + skipSeparationSpace(true, nodeIndent); + } + } + + var _line = line; + composeNode(nodeIndent, CONTEXT_FLOW_IN, false, true); + keyTag = tag; + keyNode = result; + + if ((isExplicitPair || line == _line) && CHAR_COLON == character) + { + isPair = true; + character = Utf8.charCodeAt(input, ++position); + skipSeparationSpace(true, nodeIndent); + composeNode(nodeIndent, CONTEXT_FLOW_IN, false, true); + valueNode = result; + } + + if (isMapping) + { + if (usingMaps) + storeMappingPair(_result, keyTag, keyNode, valueNode); + else + storeObjectMappingPair(_result, keyTag, keyNode, valueNode); + } + else if (isPair) + { + if (usingMaps) + _result.push(storeMappingPair(null, keyTag, keyNode, valueNode)); + else + _result.push(storeObjectMappingPair(null, keyTag, keyNode, valueNode)); + } + else + { + _result.push(keyNode); + } + + skipSeparationSpace(true, nodeIndent); + + if (CHAR_COMMA == character) + { + readNext = true; + character = Utf8.charCodeAt(input, ++position); + } + else + { + readNext = false; + } + } + + throwError('unexpected end of the stream within a flow collection'); + return false; + } + + function readBlockScalar(nodeIndent:Int) + { + var captureStart:Int; + var folding:Bool; + var chomping = CHOMPING_CLIP; + var detectedIndent = false; + var textIndent = nodeIndent; + var emptyLines = -1; + + switch (character) + { + case CHAR_VERTICAL_LINE: + folding = false; + + case CHAR_GREATER_THAN: + folding = true; + + default: + return false; + } + + kind = KIND_STRING; + result = ''; + + while (position < length) + { + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_PLUS == character || CHAR_MINUS == character) + { + if (CHOMPING_CLIP == chomping) + { + chomping = (CHAR_PLUS == character) ? CHOMPING_KEEP : CHOMPING_STRIP; + } + else + { + throwError('repeat of a chomping mode identifier'); + } + + } + else if (CHAR_DIGIT_ZERO <= character && character <= CHAR_DIGIT_NINE) + { + if (CHAR_DIGIT_ZERO == character) + { + throwError('bad explicit indentation width of a block scalar; it cannot be less than one'); + } + else if (!detectedIndent) + { + textIndent = nodeIndent + (character - CHAR_DIGIT_ONE); + detectedIndent = true; + } + else + { + throwError('repeat of an indentation width identifier'); + } + } + else + { + break; + } + } + + if (CHAR_SPACE == character || CHAR_TAB == character) + { + do { character = Utf8.charCodeAt(input, ++position); } + while (CHAR_SPACE == character || CHAR_TAB == character); + + if (CHAR_SHARP == character) + { + do { character = Utf8.charCodeAt(input, ++position); } + while (position < length && CHAR_LINE_FEED != character && CHAR_CARRIAGE_RETURN != character); + } + } + + while (position < length) + { + readLineBreak(); + lineIndent = 0; + + while ((!detectedIndent || lineIndent < textIndent) && (CHAR_SPACE == character)) + { + lineIndent += 1; + character = Utf8.charCodeAt(input, ++position); + } + + if (!detectedIndent && lineIndent > textIndent) + { + textIndent = lineIndent; + } + + if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + { + emptyLines += 1; + continue; + } + + // End of the scalar. Perform the chomping. + if (lineIndent < textIndent) + { + if (CHOMPING_KEEP == chomping) + { + #if sys + result += Utf8.encode(Strings.repeat('\n', emptyLines + 1)); + #else + result += Strings.repeat('\n', emptyLines + 1); + #end + } + else if (CHOMPING_CLIP == chomping) + { + result += '\n'; + } + break; + } + + detectedIndent = true; + + if (folding) + { + if (CHAR_SPACE == character || CHAR_TAB == character) + { + #if sys + result += Utf8.encode(Strings.repeat('\n', emptyLines + 1)); + #else + result += Strings.repeat('\n', emptyLines + 1); + #end + emptyLines = 1; + } + else if (0 == emptyLines) + { + #if sys + result += Utf8.encode(' '); + #else + result += ' '; + #end + + emptyLines = 0; + } + else + { + #if sys + result += Utf8.encode(Strings.repeat('\n', emptyLines)); + #else + result += Strings.repeat('\n', emptyLines); + #end + emptyLines = 0; + } + } + else + { + #if sys + result += Utf8.encode(Strings.repeat('\n', emptyLines + 1)); + #else + result += Strings.repeat('\n', emptyLines + 1); + #end + emptyLines = 0; + } + + captureStart = position; + + do { character = Utf8.charCodeAt(input, ++position); } + while (position < length && CHAR_LINE_FEED != character && CHAR_CARRIAGE_RETURN != character); + + captureSegment(captureStart, position, false); + } + + #if sys + result = Utf8.decode(result); + #end + + return true; + } + + function readBlockSequence(nodeIndent:Int) + { + var _line:Int; + var _tag = tag; + var _result:Array = []; + var following:Int; + var detected = false; + + if (null != anchor) + anchorMap.set(anchor, _result); + + while (position < length) + { + if (CHAR_MINUS != character) + break; + + following = Utf8.charCodeAt(input, position + 1); + + if (CHAR_SPACE != following && + CHAR_TAB != following && + CHAR_LINE_FEED != following && + CHAR_CARRIAGE_RETURN != following) + { + break; + } + + detected = true; + position += 1; + character = following; + + if (skipSeparationSpace(true, -1) != 0) + { + if (lineIndent <= nodeIndent) + { + _result.push(null); + continue; + } + } + + _line = line; + composeNode(nodeIndent, CONTEXT_BLOCK_IN, false, true); + _result.push(result); + skipSeparationSpace(true, -1); + + if ((line == _line || lineIndent > nodeIndent) && position < length) + { + throwError('bad indentation of a sequence entry'); + } + else if (lineIndent < nodeIndent) + { + break; + } + } + + if (detected) + { + tag = _tag; + kind = KIND_ARRAY; + result = _result; + return true; + } + else + { + return false; + } + } + + function readBlockMapping(nodeIndent:Int) + { + var following:Int; + var allowCompact = false; + var _line:Int; + var _tag = tag; + var _result:Dynamic = usingMaps ? new ObjectMap<{}, Dynamic>() : {}; + + var keyTag:Dynamic = null; + var keyNode:Dynamic = null; + var valueNode:Dynamic = null; + var atExplicitKey = false; + var detected = false; + + if (null != anchor) + anchorMap.set(anchor, _result); + + while (position < length) + { + following = Utf8.charCodeAt(input, position + 1); + _line = line; // Save the current line. + + if ((CHAR_QUESTION == character || + CHAR_COLON == character) && + (CHAR_SPACE == following || + CHAR_TAB == following || + CHAR_LINE_FEED == following || + CHAR_CARRIAGE_RETURN == following)) + { + if (CHAR_QUESTION == character) + { + if (atExplicitKey) + { + if (usingMaps) + storeMappingPair(_result, keyTag, keyNode, null); + else + storeObjectMappingPair(_result, keyTag, keyNode, null); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = true; + allowCompact = true; + + } + else if (atExplicitKey) + { + // i.e. CHAR_COLON == character after the explicit key. + atExplicitKey = false; + allowCompact = true; + + } + else + { + throwError('incomplete explicit mapping pair; a key node is missed'); + } + + position += 1; + character = following; + + } + else if (composeNode(nodeIndent, CONTEXT_FLOW_OUT, false, true)) + { + if (line == _line) + { + // TODO: Remove this cycle when the flow readers will consume + // trailing whitespaces like the block readers. + while (CHAR_SPACE == character || CHAR_TAB == character) + { + character = Utf8.charCodeAt(input, ++position); + } + + if (CHAR_COLON == character) + { + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character) + { + throwError('a whitespace character is expected after the key-value separator within a block mapping'); + } + + if (atExplicitKey) + { + if (usingMaps) + storeMappingPair(_result, keyTag, keyNode, null); + else + storeObjectMappingPair(_result, keyTag, keyNode, null); + + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = false; + allowCompact = false; + keyTag = tag; + keyNode = result; + + } + else if (detected) + { + throwError('can not read an implicit mapping pair; a colon is missed'); + + } + else + { + tag = _tag; + return true; // Keep the result of `composeNode`. + } + + } + else if (detected) + { + throwError('can not read a block mapping entry; a multiline key may not be an implicit key'); + } + else + { + tag = _tag; + return true; // Keep the result of `composeNode`. + } + } + else + { + break; + } + + if (line == _line || lineIndent > nodeIndent) + { + if (composeNode(nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact)) + { + if (atExplicitKey) + keyNode = result; + else + valueNode = result; + } + + if (!atExplicitKey) + { + if (usingMaps) + storeMappingPair(_result, keyTag, keyNode, valueNode); + else + storeObjectMappingPair(_result, keyTag, keyNode, valueNode); + keyTag = keyNode = valueNode = null; + } + + // TODO: It is needed only for flow node readers. It should be removed + // when the flow readers will consume trailing whitespaces as well as + // the block readers. + skipSeparationSpace(true, -1); + } + + if (lineIndent > nodeIndent && position < length) + { + throwError('bad indentation of a mapping entry'); + } + else if (lineIndent < nodeIndent) + { + break; + } + } + + if (atExplicitKey) + { + if (usingMaps) + storeMappingPair(_result, keyTag, keyNode, null); + else + storeObjectMappingPair(_result, keyTag, keyNode, null); + } + + if (detected) + { + tag = _tag; + kind = KIND_OBJECT; + result = _result; + } + + return detected; + } + + function readTagProperty() + { + var _position:Int; + var isVerbatim = false; + var isNamed = false; + var tagHandle:String = null; + var tagName:String = null; + + if (CHAR_EXCLAMATION != character) + return false; + + if (null != tag) + throwError('duplication of a tag property'); + + character = Utf8.charCodeAt(input, ++position); + + if (CHAR_LESS_THAN == character) + { + isVerbatim = true; + character = Utf8.charCodeAt(input, ++position); + + } + else if (CHAR_EXCLAMATION == character) + { + isNamed = true; + tagHandle = '!!'; + character = Utf8.charCodeAt(input, ++position); + } + else + { + tagHandle = '!'; + } + + _position = position; + + if (isVerbatim) + { + do { character = Utf8.charCodeAt(input, ++position); } + while (position < length && CHAR_GREATER_THAN != character); + + if (position < length) + { + tagName = yaml.util.Utf8.substring(input, _position, position); + character = Utf8.charCodeAt(input, ++position); + } + else + { + throwError('unexpected end of the stream within a verbatim tag'); + } + } + else + { + while (position < length && + CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character) + { + if (CHAR_EXCLAMATION == character) + { + if (!isNamed) + { + tagHandle = yaml.util.Utf8.substring(input, _position - 1, position + 1); + + if (validate && !PATTERN_TAG_HANDLE.match(tagHandle)) + { + throwError('named tag handle cannot contain such characters'); + } + + isNamed = true; + _position = position + 1; + } + else + { + throwError('tag suffix cannot contain exclamation marks'); + } + } + + character = Utf8.charCodeAt(input, ++position); + } + + tagName = yaml.util.Utf8.substring(input, _position, position); + + if (validate && PATTERN_FLOW_INDICATORS.match(tagName)) + { + throwError('tag suffix cannot contain flow indicator characters'); + } + } + + if (validate && tagName != null && tagName != "" && !PATTERN_TAG_URI.match(tagName)) + { + throwError('tag name cannot contain such characters: ' + tagName); + } + + if (isVerbatim) + { + tag = tagName; + } + else if (tagMap.exists(tagHandle)) + { + tag = tagMap.get(tagHandle) + tagName; + } + else if ('!' == tagHandle) + { + tag = '!' + tagName; + } + else if ('!!' == tagHandle) + { + tag = 'tag:yaml.org,2002:' + tagName; + } + else + { + throwError('undeclared tag handle "' + tagHandle + '"'); + } + + return true; + } + + function readAnchorProperty() + { + var _position:Int; + + if (CHAR_AMPERSAND != character) + return false; + + if (null != anchor) + throwError('duplication of an anchor property'); + + character = Utf8.charCodeAt(input, ++position); + _position = position; + + while (position < length && + CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character && + CHAR_COMMA != character && + CHAR_LEFT_SQUARE_BRACKET != character && + CHAR_RIGHT_SQUARE_BRACKET != character && + CHAR_LEFT_CURLY_BRACKET != character && + CHAR_RIGHT_CURLY_BRACKET != character) + { + character = Utf8.charCodeAt(input, ++position); + } + + if (position == _position) + throwError('name of an anchor node must contain at least one character'); + + anchor = yaml.util.Utf8.substring(input, _position, position); + return true; + } + + function readAlias() + { + var _position:Int; + var alias:String; + + if (CHAR_ASTERISK != character) + return false; + + character = Utf8.charCodeAt(input, ++position); + _position = position; + + while (position < length && + CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character && + CHAR_COMMA != character && + CHAR_LEFT_SQUARE_BRACKET != character && + CHAR_RIGHT_SQUARE_BRACKET != character && + CHAR_LEFT_CURLY_BRACKET != character && + CHAR_RIGHT_CURLY_BRACKET != character) + { + character = Utf8.charCodeAt(input, ++position); + } + + if (position == _position) + throwError('name of an alias node must contain at least one character'); + + alias = yaml.util.Utf8.substring(input, _position, position); + + if (!anchorMap.exists(alias)) + throwError('unidentified alias "' + alias + '"'); + + result = anchorMap.get(alias); + + skipSeparationSpace(true, -1); + return true; + } + + function readDocument() + { + var documentStart = position; + var _position:Int; + var directiveName:String; + var directiveArgs:Array; + var hasDirectives = false; + + version = null; + checkLineBreaks = false; + tagMap = new StringMap(); + anchorMap = new StringMap(); + + while (position < length) + { + skipSeparationSpace(true, -1); + + if (lineIndent > 0 || CHAR_PERCENT != character) + break; + + hasDirectives = true; + character = Utf8.charCodeAt(input, ++position); + _position = position; + + while (position < length && + CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character) + { + character = Utf8.charCodeAt(input, ++position); + } + + directiveName = yaml.util.Utf8.substring(input, _position, position); + directiveArgs = []; + + if (Utf8.length(directiveName) < 1) + throwError('directive name must not be less than one character in length'); + + while (position < length) + { + while (CHAR_SPACE == character || CHAR_TAB == character) + { + character = Utf8.charCodeAt(input, ++position); + } + + if (CHAR_SHARP == character) + { + do { character = Utf8.charCodeAt(input, ++position); } + while (position < length && CHAR_LINE_FEED != character && CHAR_CARRIAGE_RETURN != character); + break; + } + + if (CHAR_LINE_FEED == character || CHAR_CARRIAGE_RETURN == character) + break; + + _position = position; + + while (position < length && + CHAR_SPACE != character && + CHAR_TAB != character && + CHAR_LINE_FEED != character && + CHAR_CARRIAGE_RETURN != character) + { + character = Utf8.charCodeAt(input, ++position); + } + + directiveArgs.push(yaml.util.Utf8.substring(input, _position, position)); + } + + if (position < length) + { + readLineBreak(); + } + + if (directiveHandlers.exists(directiveName)) + { + directiveHandlers.get(directiveName)(directiveName, directiveArgs); + } + else + { + throwWarning('unknown document directive "' + directiveName + '"'); + } + } + + skipSeparationSpace(true, -1); + + if (0 == lineIndent && + CHAR_MINUS == character && + CHAR_MINUS == Utf8.charCodeAt(input, position + 1) && + CHAR_MINUS == Utf8.charCodeAt(input, position + 2)) + { + position += 3; + character = Utf8.charCodeAt(input, position); + skipSeparationSpace(true, -1); + + } + else if (hasDirectives) + { + throwError('directives end mark is expected'); + } + + composeNode(lineIndent - 1, CONTEXT_BLOCK_OUT, false, true); + skipSeparationSpace(true, -1); + + if (validate && checkLineBreaks && PATTERN_NON_ASCII_LINE_BREAKS.match(yaml.util.Utf8.substring(input, documentStart, position))) + { + throwWarning('non-ASCII line breaks are interpreted as content'); + } + + output(result); + + if (position == lineStart && testDocumentSeparator()) + { + if (CHAR_DOT == character) + { + position += 3; + character = Utf8.charCodeAt(input, position); + skipSeparationSpace(true, -1); + } + return; + } + + if (position < length) + { + throwError('end of the stream or a document separator is expected'); + } + else + { + return; + } + } + + public static inline var KIND_STRING = 'string'; + public static inline var KIND_ARRAY = 'array'; + public static inline var KIND_OBJECT = 'object'; + + + public static inline var CONTEXT_FLOW_IN = 1; + public static inline var CONTEXT_FLOW_OUT = 2; + public static inline var CONTEXT_BLOCK_IN = 3; + public static inline var CONTEXT_BLOCK_OUT = 4; + + + public static inline var CHOMPING_CLIP = 1; + public static inline var CHOMPING_STRIP = 2; + public static inline var CHOMPING_KEEP = 3; + + + public static inline var CHAR_TAB = 0x09; /* Tab */ + public static inline var CHAR_LINE_FEED = 0x0A; /* LF */ + public static inline var CHAR_CARRIAGE_RETURN = 0x0D; /* CR */ + public static inline var CHAR_SPACE = 0x20; /* Space */ + public static inline var CHAR_EXCLAMATION = 0x21; /* ! */ + public static inline var CHAR_DOUBLE_QUOTE = 0x22; /* " */ + public static inline var CHAR_SHARP = 0x23; /* # */ + public static inline var CHAR_PERCENT = 0x25; /* % */ + public static inline var CHAR_AMPERSAND = 0x26; /* & */ + public static inline var CHAR_SINGLE_QUOTE = 0x27; /* ' */ + public static inline var CHAR_ASTERISK = 0x2A; /* * */ + public static inline var CHAR_PLUS = 0x2B; /* + */ + public static inline var CHAR_COMMA = 0x2C; /* , */ + public static inline var CHAR_MINUS = 0x2D; /* - */ + public static inline var CHAR_DOT = 0x2E; /* . */ + public static inline var CHAR_SLASH = 0x2F; /* / */ + public static inline var CHAR_DIGIT_ZERO = 0x30; /* 0 */ + public static inline var CHAR_DIGIT_ONE = 0x31; /* 1 */ + public static inline var CHAR_DIGIT_NINE = 0x39; /* 9 */ + public static inline var CHAR_COLON = 0x3A; /* : */ + public static inline var CHAR_LESS_THAN = 0x3C; /* < */ + public static inline var CHAR_GREATER_THAN = 0x3E; /* > */ + public static inline var CHAR_QUESTION = 0x3F; /* ? */ + public static inline var CHAR_COMMERCIAL_AT = 0x40; /* @ */ + public static inline var CHAR_CAPITAL_A = 0x41; /* A */ + public static inline var CHAR_CAPITAL_F = 0x46; /* F */ + public static inline var CHAR_CAPITAL_L = 0x4C; /* L */ + public static inline var CHAR_CAPITAL_N = 0x4E; /* N */ + public static inline var CHAR_CAPITAL_P = 0x50; /* P */ + public static inline var CHAR_CAPITAL_U = 0x55; /* U */ + public static inline var CHAR_LEFT_SQUARE_BRACKET = 0x5B; /* [ */ + public static inline var CHAR_BACKSLASH = 0x5C; /* \ */ + public static inline var CHAR_RIGHT_SQUARE_BRACKET = 0x5D; /* ] */ + public static inline var CHAR_UNDERSCORE = 0x5F; /* _ */ + public static inline var CHAR_GRAVE_ACCENT = 0x60; /* ` */ + public static inline var CHAR_SMALL_A = 0x61; /* a */ + public static inline var CHAR_SMALL_B = 0x62; /* b */ + public static inline var CHAR_SMALL_E = 0x65; /* e */ + public static inline var CHAR_SMALL_F = 0x66; /* f */ + public static inline var CHAR_SMALL_N = 0x6E; /* n */ + public static inline var CHAR_SMALL_R = 0x72; /* r */ + public static inline var CHAR_SMALL_T = 0x74; /* t */ + public static inline var CHAR_SMALL_U = 0x75; /* u */ + public static inline var CHAR_SMALL_V = 0x76; /* v */ + public static inline var CHAR_SMALL_X = 0x78; /* x */ + public static inline var CHAR_LEFT_CURLY_BRACKET = 0x7B; /* { */ + public static inline var CHAR_VERTICAL_LINE = 0x7C; /* | */ + public static inline var CHAR_RIGHT_CURLY_BRACKET = 0x7D; /* } */ + + + public static var SIMPLE_ESCAPE_SEQUENCES:IntMap = + { + var hash = new IntMap(); + hash.set(CHAR_DIGIT_ZERO, createUtf8Char(0x00));// '\x00'); + hash.set(CHAR_SMALL_A, createUtf8Char(0x07));//'\x07'); + hash.set(CHAR_SMALL_B, createUtf8Char(0x08));//'\x08'); + hash.set(CHAR_SMALL_T, createUtf8Char(0x09));//'\x09'); + hash.set(CHAR_TAB, createUtf8Char(0x09));//'\x09'); + hash.set(CHAR_SMALL_N, createUtf8Char(0x0A));//'\x0A'); + hash.set(CHAR_SMALL_V, createUtf8Char(0x0B));//'\x0B'); + hash.set(CHAR_SMALL_F, createUtf8Char(0x0C));//'\x0C'); + hash.set(CHAR_SMALL_R, createUtf8Char(0x0D));//'\x0D'); + hash.set(CHAR_SMALL_E, createUtf8Char(0x1B));//'\x1B'); + hash.set(CHAR_SPACE, createUtf8Char(0x20));//' '); + hash.set(CHAR_DOUBLE_QUOTE, createUtf8Char(0x22));//'\x22'); + hash.set(CHAR_SLASH, createUtf8Char(0x2f));//'/'); + hash.set(CHAR_BACKSLASH, createUtf8Char(0x5C));//'\x5C'); + hash.set(CHAR_CAPITAL_N, createUtf8Char(0x85));//'\x85'); + hash.set(CHAR_UNDERSCORE, createUtf8Char(0xA0));//'\xA0'); + hash.set(CHAR_CAPITAL_L, createUtf8Char(0x2028));//'\u2028'); + hash.set(CHAR_CAPITAL_P, createUtf8Char(0x2029));//'\u2029'); + hash; + }; + + static function createUtf8Char(hex:Int):String + { + var utf8 = new Utf8(1); + utf8.addChar(hex); + return utf8.toString(); + } + + public static var HEXADECIMAL_ESCAPE_SEQUENCES:IntMap = + { + var hash = new IntMap(); + hash.set(CHAR_SMALL_X, 2); + hash.set(CHAR_SMALL_U, 4); + hash.set(CHAR_CAPITAL_U, 8); + hash; + }; + + #if (eval || neko || cpp || display) + public static var PATTERN_NON_PRINTABLE = ~/[\x{00}-\x{08}\x{0B}\x{0C}\x{0E}-\x{1F}\x{7F}-\x{84}\x{86}-\x{9F}\x{FFFE}\x{FFFF}]/u; + #elseif (js || flash9 || java) + public static var PATTERN_NON_PRINTABLE = ~/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uD800-\uDFFF\uFFFE\uFFFF]/u; + #else + #error "Compilation target not supported due to lack of Unicode RegEx support." + #end + + #if (eval || neko || cpp || display) + public static var PATTERN_NON_ASCII_LINE_BREAKS = ~/[\x{85}\x{2028}\x{2029}]/u; + #elseif (js || flash9 || java) + public static var PATTERN_NON_ASCII_LINE_BREAKS = ~/[\x85\u2028\u2029]/u; + #else + #error "Compilation target not supported due to lack of Unicode RegEx support." + #end + + public static var PATTERN_FLOW_INDICATORS = ~/[,\[\]\{\}]/u; + public static var PATTERN_TAG_HANDLE = ~/^(?:!|!!|![a-z\-]+!)$/iu; + public static var PATTERN_TAG_URI = ~/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/iu; + +} diff --git a/src/main/yaml/Renderer.hx b/src/main/yaml/Renderer.hx new file mode 100644 index 0000000..8d6e8d1 --- /dev/null +++ b/src/main/yaml/Renderer.hx @@ -0,0 +1,638 @@ +package yaml; + +import Type; +import yaml.util.StringMap; +import yaml.util.IntMap; +import haxe.Utf8; +import haxe.io.Bytes; +import yaml.YamlType; +import yaml.schema.DefaultSchema; +import yaml.schema.SafeSchema; +import yaml.util.Strings; +import yaml.util.Ints; + +import yaml.YamlException; + +class RenderOptions +{ + public var schema:Schema; + public var indent:Int; + public var flow:Int; + public var styles:StringMap; + + public function new(?schema:Schema, ?styles:StringMap) + { + this.schema = (schema != null) ? schema : new DefaultSchema(); + this.styles = (styles != null) ? styles : new StringMap(); + this.indent = 2; + this.flow = -1; + } + + public function setSchema(schema:Schema):RenderOptions + { + this.schema = schema; + return this; + } + + public function setFlowLevel(level:Int):RenderOptions + { + this.flow = level; + return this; + } + + /** + The indentation level. Default is 2. + */ + public function setIndent(indent:Int):RenderOptions + { + this.indent = indent; + return this; + } + + public function setStyle(name:String, value:String):RenderOptions + { + styles.set(name, value); + return this; + } +} + +class Renderer +{ + /** + Utility method to create RenderOptions for configuring a Renderer instance. + */ + public static function options():RenderOptions + { + return new RenderOptions(); + } + + var schema:Schema; + var indent:Int; + var flowLevel:Int; + var styleMap:StringMap; + + var implicitTypes:Array; + var explicitTypes:Array; + + var kind:String; + var tag:String; + var result:Dynamic; + + public function new() + {} + + public function safeRender(input:Dynamic, options:RenderOptions) + { + options.schema = new SafeSchema(); + return render(input, options); + } + + public function render(input:Dynamic, options:RenderOptions) + { + schema = options.schema; + indent = Std.int(Math.max(1, options.indent)); + flowLevel = options.flow; + styleMap = compileStyleMap(schema, options.styles); + + implicitTypes = schema.compiledImplicit; + explicitTypes = schema.compiledExplicit; + + writeNode(0, input, true, true); + + return result + '\n'; + } + + function generateNextLine(level:Int) + { + return '\n' + Strings.repeat(' ', indent * level); + } + + function testImplicitResolving(object:Dynamic) + { + for (type in implicitTypes) + { + try + { + if (!type.loader.skip) + { + type.resolve(object, false); + return true; + } + } + catch(e:ResolveTypeException) {} + } + + return false; + } + + function writeScalar(object:String) + { + #if sys + object = Utf8.encode(object); + #end + + var isQuoted = false; + var checkpoint = 0; + var position = -1; + + result = ''; + + if (0 == object.length || + CHAR_SPACE == Utf8.charCodeAt(object, 0) || + CHAR_SPACE == Utf8.charCodeAt(object, Utf8.length(object) - 1)) + { + isQuoted = true; + } + + var length = Utf8.length(object); + while (++position < length) + { + var character = Utf8.charCodeAt(object, position); + if (!isQuoted) + { + if (CHAR_TAB == character || + CHAR_LINE_FEED == character || + CHAR_CARRIAGE_RETURN == character || + CHAR_COMMA == character || + CHAR_LEFT_SQUARE_BRACKET == character || + CHAR_RIGHT_SQUARE_BRACKET == character || + CHAR_LEFT_CURLY_BRACKET == character || + CHAR_RIGHT_CURLY_BRACKET == character || + CHAR_SHARP == character || + CHAR_AMPERSAND == character || + CHAR_ASTERISK == character || + CHAR_EXCLAMATION == character || + CHAR_VERTICAL_LINE == character || + CHAR_GREATER_THAN == character || + CHAR_SINGLE_QUOTE == character || + CHAR_DOUBLE_QUOTE == character || + CHAR_PERCENT == character || + CHAR_COMMERCIAL_AT == character || + CHAR_GRAVE_ACCENT == character || + CHAR_QUESTION == character || + CHAR_COLON == character || + CHAR_MINUS == character) + { + isQuoted = true; + } + } + + if (ESCAPE_SEQUENCES.exists(character) || + !((0x00020 <= character && character <= 0x00007E) || + (0x00085 == character) || + (0x000A0 <= character && character <= 0x00D7FF) || + (0x0E000 <= character && character <= 0x00FFFD) || + (0x10000 <= character && character <= 0x10FFFF))) + { + result += yaml.util.Utf8.substring(object, checkpoint, position); + + if (ESCAPE_SEQUENCES.exists(character)) + { + result += ESCAPE_SEQUENCES.get(character); + } + else + { + result += encodeHex(character); + } + + checkpoint = position + 1; + isQuoted = true; + } + } + + if (checkpoint < position) + { + result += yaml.util.Utf8.substring(object, checkpoint, position); + } + + if (!isQuoted && testImplicitResolving(result)) + { + isQuoted = true; + } + + if (isQuoted) + { + result = '"' + result + '"'; + } + + #if sys + result = Utf8.decode(result); + #end + } + + function writeFlowSequence(level:Int, object:Array) + { + var _result = ''; + var _tag = tag; + + for (index in 0...object.length) + { + if (0 != index) + _result += ', '; + + writeNode(level, object[index], false, false); + _result += result; + } + + tag = _tag; + result = '[' + _result + ']'; + } + + function writeBlockSequence(level:Int, object:Array, compact:Bool) + { + var _result = ''; + var _tag = tag; + + for (index in 0...object.length) + { + if (!compact || 0 != index) + _result += generateNextLine(level); + + writeNode(level + 1, object[index], true, true); + _result += '- ' + result; + } + + tag = _tag; + result = _result; + } + + function writeFlowMapping(level:Int, object:Dynamic) + { + if (Type.typeof(object) == ValueType.TObject) + writeObjectFlowMapping(level, object); + else + writeMapFlowMapping(level, object); + } + + function writeObjectFlowMapping(level:Int, object:Dynamic) + { + var _result = ''; + var _tag = tag; + var index = 0; + var objectKey; + + for (objectKey in Reflect.fields(object)) + { + if (0 != index++) + _result += ', '; + + var objectValue = Reflect.field(object, objectKey); + + writeNode(level, objectKey, false, false); + + if (result.length > 1024) + _result += '? '; + + _result += result + ': '; + writeNode(level, objectValue, false, false); + _result += result; + } + + tag = _tag; + result = '{' + _result + '}'; + } + + #if haxe3 + function writeMapFlowMapping(level:Int, object:Map) + #else + function writeMapFlowMapping(level:Int, object:Dynamic) + #end + { + var _result = ''; + var _tag = tag; + var index = 0; + var objectKey; + var keys:Iterator = object.keys(); + + for (objectKey in keys) + { + if (0 != index++) + _result += ', '; + + var objectValue = object.get(objectKey); + + writeNode(level, objectKey, false, false); + + if (result.length > 1024) + _result += '? '; + + _result += result + ': '; + writeNode(level, objectValue, false, false); + _result += result; + } + + tag = _tag; + result = '{' + _result + '}'; + } + + function writeBlockMapping(level:Int, object:Dynamic, compact:Bool) + { + if (Type.typeof(object) == ValueType.TObject) + writeObjectBlockMapping(level, object, compact); + else + writeMapBlockMapping(level, object, compact); + } + + function writeObjectBlockMapping(level:Int, object:Dynamic, compact:Bool) + { + var _result = ''; + var _tag = tag; + var index = 0; + + for (objectKey in Reflect.fields(object)) + { + if (!compact || 0 != index++) + _result += generateNextLine(level); + + var objectValue = Reflect.field(object, objectKey); + writeNode(level + 1, objectKey, true, true); + var explicitPair = (null != tag && '?' != tag && result.length <= 1024); + + if (explicitPair) + _result += '? '; + + _result += result; + + if (explicitPair) + _result += generateNextLine(level); + + writeNode(level + 1, objectValue, true, explicitPair); + _result += ': ' + result; + } + + tag = _tag; + result = _result; + } + + #if haxe3 + function writeMapBlockMapping(level:Int, object:Map, compact:Bool) + #else + function writeMapBlockMapping(level:Int, object:Dynamic, compact:Bool) + #end + { + var _result = ''; + var _tag = tag; + var index = 0; + var keys:Iterator = object.keys(); + + for (objectKey in keys) + { + if (!compact || 0 != index++) + _result += generateNextLine(level); + + var objectValue = object.get(objectKey); + writeNode(level + 1, objectKey, true, true); + var explicitPair = (null != tag && '?' != tag && result.length <= 1024); + + if (explicitPair) + _result += '? '; + + _result += result; + + if (explicitPair) + _result += generateNextLine(level); + + writeNode(level + 1, objectValue, true, explicitPair); + _result += ': ' + result; + } + + tag = _tag; + result = _result; + } + + function detectType(object:Dynamic, explicit:Bool) + { + var _result:Dynamic = null; + var typeList:Array = explicit ? explicitTypes : implicitTypes; + var style:String; + + kind = kindOf(object); + + for (type in typeList) + { + if ((null != type.dumper) && + type.dumper.skip != true && + (null == type.dumper.kind || kind == type.dumper.kind) && + (null == type.dumper.instanceOf || Std.is(object, type.dumper.instanceOf) && + (null == type.dumper.predicate || type.dumper.predicate(object)))) + { + tag = explicit ? type.tag : '?'; + + if (styleMap.exists(type.tag)) + style = styleMap.get(type.tag); + else + style = type.dumper.defaultStyle; + + var success = true; + try + { + _result = type.represent(object, style); + } + catch (e:RepresentTypeException) + { + success = false; + } + + if (success) + { + kind = kindOf(_result); + result = _result; + } + else + { + if (explicit) + throw new YamlException('cannot represent an object of !<' + type.tag + '> type'); + else + continue; + } + return true; + } + } + return false; + } + + function writeNode(level:Int, object:Dynamic, block:Bool, compact:Bool) + { + tag = null; + result = object; + + if (!detectType(object, false)) + detectType(object, true); + + if (block) + block = (0 > flowLevel || flowLevel > level); + + if ((null != tag && '?' != tag) || (2 != indent && level > 0)) + compact = false; + + if ('object' == kind) + { + var empty = (Type.typeof(object) == ValueType.TObject) ? (Reflect.fields(object).length == 0) : Lambda.empty(object); + + if (block && !empty) + { + writeBlockMapping(level, object, compact); + } + else + { + writeFlowMapping(level, object); + } + } + else if ('array' == kind) + { + if (block && (0 != result.length)) + { + writeBlockSequence(level, result, compact); + } + else + { + writeFlowSequence(level, result); + } + } + else if ('string' == kind) + { + if ('?' != tag) + { + writeScalar(result); + } + } + else + { + throw new YamlException('unacceptabe kind of an object to dump (' + kind + ')'); + } + + if (null != tag && '?' != tag) + { + result = '!<' + tag + '> ' + result; + } + } + + public function kindOf(object:Dynamic) + { + var kind = Type.typeof(object); + + return switch (Type.typeof(object)) + { + case TNull: "null"; + case TInt: "integer"; + case TFloat: "float"; + case TBool: "boolean"; + case TObject: + if (Std.is(object, Array)) "array"; + else "object"; + case TFunction: "function"; + case TClass(c): + if (c == String) "string"; + else if (c == Array) "array"; + else if (c == Bytes) "binary"; + else "object"; + case TEnum(_): "enum"; + case TUnknown: "unknown"; + } + } + + static function compileStyleMap(schema:Schema, map:StringMap):StringMap + { + if (null == map) + return new StringMap(); + + var result = new StringMap(); + + for (tag in map.keys()) + { + var style = Std.string(map.get(tag)); + + if (0 == tag.indexOf('!!')) + tag = 'tag:yaml.org,2002:' + tag.substring(2); + + var type = schema.compiledTypeMap.get(tag); + + if (type != null && type.dumper != null) + { + if (type.dumper.styleAliases.exists(style)) + { + style = type.dumper.styleAliases.get(style); + } + } + result.set(tag, style); + } + + return result; + } + + static function encodeHex(charCode:Int) + { + var handle:String; + var length:Int; + var str = Ints.toString(charCode, 16).toUpperCase(); + + if (charCode <= 0xFF) + { + handle = 'x'; + length = 2; + } + else if (charCode <= 0xFFFF) + { + handle = 'u'; + length = 4; + } + else if (charCode <= 0xFFFFFFFF) + { + handle = 'U'; + length = 8; + } + else + { + throw new YamlException('code point within a string may not be greater than 0xFFFFFFFF'); + } + + return '\\' + handle + Strings.repeat('0', length - str.length) + str; + } + + static inline var CHAR_TAB = 0x09; /* Tab */ + static inline var CHAR_LINE_FEED = 0x0A; /* LF */ + static inline var CHAR_CARRIAGE_RETURN = 0x0D; /* CR */ + static inline var CHAR_SPACE = 0x20; /* Space */ + static inline var CHAR_EXCLAMATION = 0x21; /* ! */ + static inline var CHAR_DOUBLE_QUOTE = 0x22; /* " */ + static inline var CHAR_SHARP = 0x23; /* # */ + static inline var CHAR_PERCENT = 0x25; /* % */ + static inline var CHAR_AMPERSAND = 0x26; /* & */ + static inline var CHAR_SINGLE_QUOTE = 0x27; /* ' */ + static inline var CHAR_ASTERISK = 0x2A; /* * */ + static inline var CHAR_COMMA = 0x2C; /* , */ + static inline var CHAR_MINUS = 0x2D; /* - */ + static inline var CHAR_COLON = 0x3A; /* : */ + static inline var CHAR_GREATER_THAN = 0x3E; /* > */ + static inline var CHAR_QUESTION = 0x3F; /* ? */ + static inline var CHAR_COMMERCIAL_AT = 0x40; /* @ */ + static inline var CHAR_LEFT_SQUARE_BRACKET = 0x5B; /* [ */ + static inline var CHAR_RIGHT_SQUARE_BRACKET = 0x5D; /* ] */ + static inline var CHAR_GRAVE_ACCENT = 0x60; /* ` */ + static inline var CHAR_LEFT_CURLY_BRACKET = 0x7B; /* { */ + static inline var CHAR_VERTICAL_LINE = 0x7C; /* | */ + static inline var CHAR_RIGHT_CURLY_BRACKET = 0x7D; /* } */ + static var HEX_VALUES = "0123456789ABCDEF"; + + + static var ESCAPE_SEQUENCES:IntMap = + { + var hash = new IntMap(); + hash.set(0x00, '\\0'); + hash.set(0x07, '\\a'); + hash.set(0x08, '\\b'); + hash.set(0x09, '\\t'); + hash.set(0x0A, '\\n'); + hash.set(0x0B, '\\v'); + hash.set(0x0C, '\\f'); + hash.set(0x0D, '\\r'); + hash.set(0x1B, '\\e'); + hash.set(0x22, '\\"'); + hash.set(0x5C, '\\\\'); + hash.set(0x85, '\\N'); + hash.set(0xA0, '\\_'); + hash.set(0x2028, '\\L'); + hash.set(0x2029, '\\P'); + hash; + }; +} diff --git a/src/main/yaml/Schema.hx b/src/main/yaml/Schema.hx new file mode 100644 index 0000000..183813e --- /dev/null +++ b/src/main/yaml/Schema.hx @@ -0,0 +1,95 @@ +package yaml; + +import yaml.util.StringMap; +import yaml.YamlType; + +class Schema +{ + public static var DEFAULT:Schema; + + public var compiledImplicit:Array; + public var compiledExplicit:Array; + public var compiledTypeMap:StringMap; + + public var implicit:Array; + public var explicit:Array; + public var include:Array; + + public function new(include:Array, explicit:Array, ?implicit:Array) + { + this.include = (include == null) ? [] : include; + this.implicit = (implicit == null) ? [] : implicit; + this.explicit = (explicit == null) ? [] : explicit; + + for (type in this.implicit) + { + if (null != type.loader && 'string' != type.loader.kind) + { + throw new YamlException('There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.'); + } + } + + this.compiledImplicit = compileList(this, 'implicit', []); + this.compiledExplicit = compileList(this, 'explicit', []); + this.compiledTypeMap = compileMap([this.compiledImplicit, this.compiledExplicit]); + } + + public static function create(types:Array, ?schemas:Array) + { + if (schemas == null) + schemas = [DEFAULT]; + else if (schemas.length == 0) + schemas.push(DEFAULT); + + return new Schema(schemas, types); + } + + + public static function compileList(schema:Schema, name:String, result:Array) + { + var exclude = []; + + for (includedSchema in schema.include) + { + result = compileList(includedSchema, name, result); + } + + var types:Array = switch (name) + { + case "implicit": schema.implicit; + case "explicit": schema.explicit; + default: throw new YamlException("unknown type list type: " + name); + } + + for (currenYamlType in types) + { + for (previousIndex in 0...result.length) + { + var previousType = result[previousIndex]; + if (previousType.tag == currenYamlType.tag) + { + exclude.push(previousIndex); + } + } + result.push(currenYamlType); + } + + var filteredResult:Array = []; + for (i in 0...result.length) + if (!Lambda.has(exclude, i)) + filteredResult.push(result[i]); + + return filteredResult; + } + + public static function compileMap(list:Array>):StringMap + { + var result = new StringMap(); + + for (member in list) + for (type in member) + result.set(type.tag, type); + + return result; + } +} diff --git a/src/main/yaml/Yaml.hx b/src/main/yaml/Yaml.hx new file mode 100644 index 0000000..a6c4d37 --- /dev/null +++ b/src/main/yaml/Yaml.hx @@ -0,0 +1,69 @@ +package yaml; + +import yaml.Renderer; +import yaml.Parser; + +/** +Facade for common YAML processing operations. +*/ +class Yaml +{ + /** + Parse a yaml document into object form. + + @param document The yaml document to parse. + @param ?options Parsing options (optional). + + @return The parsed yaml document in object form. + */ + public static function parse(document:String, ?options:ParserOptions):Dynamic + { + if (options == null) options = new ParserOptions(); + return new Parser().parse(document, options); + } + + #if sys + /** + Read a yaml document from disk and parse into object form. + + @param filePath The path to read the yaml file. + @param ?options Parsing options (optional). + + @return The parsed yaml document in object form. + */ + public static function read(filePath:String, ?options:ParserOptions):Dynamic + { + return parse(sys.io.File.getContent(filePath), options); + } + #end + + /** + Render a yaml object graph as a yaml text document. + + @param data The root object to render. + @param ?options Rendering options (optional). + + @return The rendered yaml document. + */ + public static function render(data:Dynamic, ?options:RenderOptions):String + { + if (options == null) options = new RenderOptions(); + return new Renderer().render(data, options); + } + + #if sys + /** + Render an object graph as a yaml text document and write it to disk. + + @param filePath The path to write the yaml document. + @param data The root object to render. + @param ?options Rendering options (optional). + */ + public static function write(filePath:String, data:Dynamic, ?options:RenderOptions):Void + { + sys.io.File.saveContent(filePath, render(data, options)); + } + #end + + private function new() {} +} diff --git a/src/main/yaml/YamlException.hx b/src/main/yaml/YamlException.hx new file mode 100644 index 0000000..72d93de --- /dev/null +++ b/src/main/yaml/YamlException.hx @@ -0,0 +1,31 @@ +package yaml; + +import haxe.PosInfos; + +class YamlException +{ + public var name(get, null):String; + function get_name():String { return name; } + + public var message(get, null):String; + function get_message():String { return message; } + + public var cause(default, null):Dynamic; + public var info(default, null):PosInfos; + + public function new(?message:String="", ?cause:Dynamic = null, ?info:PosInfos) + { + this.name = Type.getClassName(Type.getClass(this)); + this.message = message; + this.cause = cause; + this.info = info; + } + + public function toString():String + { + var str:String = name + ": " + message; + if (info != null) + str += " at " + info.className + "#" + info.methodName + " (" + info.lineNumber + ")"; + return str; + } +} diff --git a/src/main/yaml/YamlType.hx b/src/main/yaml/YamlType.hx new file mode 100644 index 0000000..8354293 --- /dev/null +++ b/src/main/yaml/YamlType.hx @@ -0,0 +1,112 @@ +package yaml; + +import yaml.util.StringMap; +import haxe.PosInfos; + +typedef POptions = +{ + ?kind:String, + ?skip:Bool +} + +typedef ROptions = +{ + ?kind:String, + ?defaultStyle:String, + ?instanceOf:Class, + ?predicate:Dynamic->Bool, + ?styleAliases:StringMap, + ?skip:Bool +} + +typedef AnyYamlType = YamlType; +typedef StringYamlType = YamlType; + +class YamlType +{ + public var tag:String; + public var loader:POptions; + public var dumper:ROptions; + + public function new(tag:String, loaderOptions:POptions, dumperOptions:ROptions) + { + if (loaderOptions == null && dumperOptions == null) + throw new YamlException('Incomplete YAML type definition. "loader" or "dumper" setting must be specified.'); + + this.tag = tag; + this.loader = loaderOptions; + this.dumper = dumperOptions; + + if (loaderOptions != null && !loaderOptions.skip) + validateLoaderOptions(); + if (dumperOptions != null && !dumperOptions.skip) + validateDumperOptions(); + } + + public function resolve(object:D, ?usingMaps:Bool = true, ?explicit:Bool = false):T + { + cantResolveType(); + return null; + } + + public function represent(object:T, ?style:String):String + { + cantRepresentType(); + return null; + } + + function cantResolveType(?info:PosInfos):Dynamic + { + throw new ResolveTypeException("", null, info); + return null; + } + + function cantRepresentType(?info:PosInfos):Dynamic + { + throw new RepresentTypeException("", null, info); + return null; + } + + function validateLoaderOptions() + { + if (loader.skip != true && 'string' != loader.kind && 'array' != loader.kind && 'object' != loader.kind) + { + throw new YamlException('Unacceptable "kind" setting of a type loader: ' + loader.kind); + } + } + + function validateDumperOptions() + { + if (dumper.skip != true && + 'undefined' != dumper.kind && + 'null' != dumper.kind && + 'boolean' != dumper.kind && + 'integer' != dumper.kind && + 'float' != dumper.kind && + 'string' != dumper.kind && + 'array' != dumper.kind && + 'object' != dumper.kind && + 'binary' != dumper.kind && + 'function' != dumper.kind) + { + throw new YamlException('Unacceptable "kind" setting of a type dumper: ' + dumper.kind); + } + } +} + +class ResolveTypeException extends YamlException +{ + public function new(?message:String="", ?cause:Dynamic = null, ?info:PosInfos) + { + super(message, cause, info); + } +} + +class RepresentTypeException extends YamlException +{ + public function new(?message:String="", ?cause:Dynamic = null, ?info:PosInfos) + { + super(message, cause, info); + } +} + diff --git a/src/main/yaml/schema/DefaultSchema.hx b/src/main/yaml/schema/DefaultSchema.hx new file mode 100644 index 0000000..14b6b17 --- /dev/null +++ b/src/main/yaml/schema/DefaultSchema.hx @@ -0,0 +1,11 @@ +package yaml.schema; + +import yaml.Schema; + +class DefaultSchema extends Schema +{ + public function new() + { + super([new SafeSchema()], null);//[new TRegex(), new TFunction()]); + } +} diff --git a/src/main/yaml/schema/MinimalSchema.hx b/src/main/yaml/schema/MinimalSchema.hx new file mode 100644 index 0000000..0c2bd2a --- /dev/null +++ b/src/main/yaml/schema/MinimalSchema.hx @@ -0,0 +1,14 @@ +package yaml.schema; + +import yaml.type.YSeq; +import yaml.type.YMap; +import yaml.type.YString; +import yaml.Schema; + +class MinimalSchema extends Schema +{ + public function new() + { + super([], [new YString(), new YSeq(), new YMap()]); + } +} diff --git a/src/main/yaml/schema/SafeSchema.hx b/src/main/yaml/schema/SafeSchema.hx new file mode 100644 index 0000000..c86b817 --- /dev/null +++ b/src/main/yaml/schema/SafeSchema.hx @@ -0,0 +1,24 @@ +package yaml.schema; + +import yaml.type.YOmap; +import yaml.type.YMerge; +import yaml.type.YTimestamp; +import yaml.type.YSet; +import yaml.type.YPairs; +import yaml.type.YInt; +import yaml.type.YNull; +import yaml.schema.MinimalSchema; +import yaml.type.YFloat; +import yaml.type.YBool; +import yaml.type.YBinary; +import yaml.Schema; + +class SafeSchema extends Schema +{ + public function new() + { + super([new MinimalSchema()], + [new YBinary(), new YOmap(), new YPairs(), new YSet()], + [new YNull(), new YBool(), new YInt(), new YFloat(), new YTimestamp(), new YMerge()]); + } +} diff --git a/src/main/yaml/type/YBinary.hx b/src/main/yaml/type/YBinary.hx new file mode 100644 index 0000000..399f750 --- /dev/null +++ b/src/main/yaml/type/YBinary.hx @@ -0,0 +1,119 @@ +package yaml.type; + +import haxe.io.Bytes; +import haxe.Utf8; +import yaml.YamlType; + +class YBinary extends yaml.StringYamlType +{ + static inline var BASE64_PADDING_CODE = 0x3D; + static inline var BASE64_PADDING_CHAR = '='; + + static var BASE64_BINTABLE = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 + ]; + + static var BASE64_CHARTABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); + + public function new() + { + super('tag:yaml.org,2002:binary', {kind:"string"}, {kind:"binary", instanceOf:Bytes}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool):Bytes + { + var length = Utf8.length(object); + var idx = 0; + var result = []; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (idx in 0...length) + { + var code = Utf8.charCodeAt(object, idx); + var value = BASE64_BINTABLE[code & 0x7F]; + + // Skip LF(NL) || CR + if (0x0A != code && 0x0D != code) + { + // Fail on illegal characters + if (-1 == value) + return cantResolveType(); + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | value; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + + // Append if not padding. + if (BASE64_PADDING_CODE != code) { + result.push((leftdata >> leftbits) & 0xFF); + } + + leftdata &= (1 << leftbits) - 1; + } + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits != 0) + cantResolveType(); + + var bytes = Bytes.alloc(result.length); + for (i in 0...result.length) + bytes.set(i, result[i]); + + return bytes; + } + + override public function represent(object:Bytes, ?style:String):String + { + var result = ''; + var index = 0; + var max = object.length - 2; + + // Convert every three bytes to 4 ASCII characters. + while (index < max) + { + result += BASE64_CHARTABLE[object.get(index + 0) >> 2]; + result += BASE64_CHARTABLE[((object.get(index + 0) & 0x03) << 4) + (object.get(index + 1) >> 4)]; + result += BASE64_CHARTABLE[((object.get(index + 1) & 0x0F) << 2) + (object.get(index + 2) >> 6)]; + result += BASE64_CHARTABLE[object.get(index + 2) & 0x3F]; + index += 3; + } + + var rest = object.length % 3; + + // Convert the remaining 1 or 2 bytes, padding out to 4 characters. + if (0 != rest) + { + index = object.length - rest; + result += BASE64_CHARTABLE[object.get(index + 0) >> 2]; + + if (2 == rest) + { + result += BASE64_CHARTABLE[((object.get(index + 0) & 0x03) << 4) + (object.get(index + 1) >> 4)]; + result += BASE64_CHARTABLE[(object.get(index + 1) & 0x0F) << 2]; + result += BASE64_PADDING_CHAR; + } + else + { + result += BASE64_CHARTABLE[(object.get(index + 0) & 0x03) << 4]; + result += BASE64_PADDING_CODE + BASE64_PADDING_CHAR; + } + } + + return result; + } +} diff --git a/src/main/yaml/type/YBool.hx b/src/main/yaml/type/YBool.hx new file mode 100644 index 0000000..5802384 --- /dev/null +++ b/src/main/yaml/type/YBool.hx @@ -0,0 +1,92 @@ +package yaml.type; + +import yaml.util.StringMap; +import yaml.YamlException; +import yaml.YamlType; + +class YBool extends StringYamlType +{ + static var YAML_IMPLICIT_BOOLEAN_MAP:StringMap = + { + var hash = new StringMap(); + hash.set("true", true); + hash.set("True", true); + hash.set("TRUE", true); + hash.set("false", false); + hash.set("False", false); + hash.set("FALSE", false); + hash; + }; + + static var YAML_EXPLICIT_BOOLEAN_MAP = + { + var hash = new StringMap(); + hash.set("true", true); + hash.set("True", true); + hash.set("TRUE", true); + hash.set("false", false); + hash.set("False", false); + hash.set("FALSE", false); + hash.set("y", true); + hash.set("Y", true); + hash.set("yes", true); + hash.set("Yes", true); + hash.set("YES", true); + hash.set("n", false); + hash.set("N", false); + hash.set("no", false); + hash.set("No", false); + hash.set("NO", false); + hash.set("on", true); + hash.set("On", true); + hash.set("ON", true); + hash.set("off", false); + hash.set("Off", false); + hash.set("OFF", false); + hash; + }; + + public function new() + { + super('tag:yaml.org,2002:bool', {kind:"string"}, {kind:"boolean", defaultStyle:"lowercase"}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool):Bool + { + if (explicit) + { + if (YAML_EXPLICIT_BOOLEAN_MAP.exists(object)) + { + return YAML_EXPLICIT_BOOLEAN_MAP.get(object); + } + else + { + return cantResolveType(); + } + } + else + { + if (YAML_IMPLICIT_BOOLEAN_MAP.exists(object)) + { + return YAML_IMPLICIT_BOOLEAN_MAP.get(object); + } + else + { + return cantResolveType(); + } + } + } + + override public function represent(object:Bool, ?style:String):String + { + return switch (style) + { + case "uppercase": object ? 'TRUE' : 'FALSE'; + case "lowercase": object ? 'true' : 'false'; + case "camelcase": object ? 'True' : 'False'; + default: + throw new YamlException("Style not supported: " + style); + null; + } + } +} diff --git a/src/main/yaml/type/YFloat.hx b/src/main/yaml/type/YFloat.hx new file mode 100644 index 0000000..ecce9f9 --- /dev/null +++ b/src/main/yaml/type/YFloat.hx @@ -0,0 +1,107 @@ +package yaml.type; + +import yaml.YamlType; +import yaml.util.Floats; + +class YFloat extends yaml.StringYamlType> +{ + static var YAML_FLOAT_PATTERN = new EReg( + '^(?:[-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+][0-9]+)?' + + '|\\.[0-9_]+(?:[eE][-+][0-9]+)?' + + '|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*' + + '|[-+]?\\.(?:inf|Inf|INF)' + + '|\\.(?:nan|NaN|NAN))$', "iu"); + + public function new() + { + super('tag:yaml.org,2002:float', {kind:"string"}, {kind:"float", defaultStyle:"lowercase"}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool):Null + { + if (!YAML_FLOAT_PATTERN.match(object)) + cantResolveType(); + + var value:String = StringTools.replace(object, '_', '').toLowerCase(); + var sign = ('-' == value.charAt(0)) ? -1 : 1; + + if (0 <= '+-'.indexOf(value.charAt(0))) + { + value = value.substr(1); + } + + if ('.inf' == value) + { + return (1 == sign) ? Math.POSITIVE_INFINITY : Math.NEGATIVE_INFINITY; + } + else if ('.nan' == value) + { + return Math.NaN; + } + else if (0 <= value.indexOf(':')) + { + var digits:Array = []; + for (v in value.split(':')) + { + digits.unshift(Std.parseFloat(v)); + } + + var v = 0.0; + var base = 1; + + for (d in digits) + { + v += d * base; + base *= 60; + } + + return sign * v; + + } + else + { + return sign * Std.parseFloat(value); + } + } + + + override public function represent(object:Null, ?style:String):String + { + if (Math.isNaN(object)) + { + return switch (style) + { + case 'lowercase': '.nan'; + case 'uppercase': '.NAN'; + case 'camelcase': '.NaN'; + default: ".nan"; + } + } + else if (Math.POSITIVE_INFINITY == object) + { + return switch (style) + { + case 'lowercase': '.inf'; + case 'uppercase': '.INF'; + case 'camelcase': '.Inf'; + default: ".inf"; + } + } + else if (Math.NEGATIVE_INFINITY == object) + { + return switch (style) + { + case 'lowercase': '-.inf'; + case 'uppercase': '-.INF'; + case 'camelcase': '-.Inf'; + default: "-.inf"; + } + } + else + { + return Floats.toString(object); + } + } +} + + diff --git a/src/main/yaml/type/YInt.hx b/src/main/yaml/type/YInt.hx new file mode 100644 index 0000000..69e58b2 --- /dev/null +++ b/src/main/yaml/type/YInt.hx @@ -0,0 +1,103 @@ +package yaml.type; + +import yaml.YamlException; +import yaml.util.StringMap; +import yaml.util.Ints; +import yaml.YamlType; + +class YInt extends yaml.StringYamlType> +{ + static var YAML_INTEGER_PATTERN = new EReg( + '^(?:[-+]?0b[0-1_]+' + + '|[-+]?0[0-7_]+' + + '|[-+]?(?:0|[1-9][0-9_]*)' + + '|[-+]?0x[0-9a-fA-F_]+' + + '|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$', "iu"); + + public function new() + { + super('tag:yaml.org,2002:int', {kind:"string"}, {kind:"integer", defaultStyle:"decimal", styleAliases:createStyleAliases()}); + } + + function createStyleAliases() + { + var styleAliases = new StringMap(); + styleAliases.set('bin', "binary"); + styleAliases.set('2', "binary"); + styleAliases.set('oct', "octal"); + styleAliases.set('8', "octal"); + styleAliases.set('dec', "decimal"); + styleAliases.set('hex', "hexadecimal"); + styleAliases.set('16', "hexadecimal"); + return styleAliases; + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool):Null + { + if (!YAML_INTEGER_PATTERN.match(object)) + cantResolveType(); + + var value = StringTools.replace(object, '_', '').toLowerCase(); + var sign = ('-' == value.charAt(0)) ? -1 : 1; + var digits = []; + + if (0 <= '+-'.indexOf(value.charAt(0))) + { + value = value.substr(1); + } + + if ('0' == value) + { + return 0; + } + else if (value.indexOf("0b") == 0) + { + return sign * Ints.parseInt(value.substr(2), 2); + + } + else if (value.indexOf("0x") == 0) + { + return sign * Ints.parseInt(value, 16); + } + else if (value.indexOf("0") == 0) + { + return sign * Ints.parseInt(value, 8); + } + else if (0 <= value.indexOf(':')) // base 60 + { + for (v in value.split(':')) + { + digits.unshift(Ints.parseInt(v, 10)); + } + + var result = 0; + var base = 1; + + for (d in digits) + { + result += (d * base); + base *= 60; + } + + return sign * result; + } + else + { + return sign * Ints.parseInt(value, 10); + } + } + + override public function represent(object:Null, ?style:String):String + { + return switch (style) + { + case "binary": '0b' + Ints.toString(object, 2); + case "octal": '0' + Ints.toString(object, 8); + case "decimal": Ints.toString(object, 10); + case "hexadecimal": '0x' + Ints.toString(object, 16); + default: + throw new YamlException("Style not supported: " + style); + null; + } + } +} diff --git a/src/main/yaml/type/YMap.hx b/src/main/yaml/type/YMap.hx new file mode 100644 index 0000000..988c8b9 --- /dev/null +++ b/src/main/yaml/type/YMap.hx @@ -0,0 +1,12 @@ +package yaml.type; + +import yaml.util.ObjectMap; +import yaml.YamlType; + +class YMap extends yaml.StringYamlType +{ + public function new() + { + super('tag:yaml.org,2002:map', {kind:"object", skip:true}, {skip:true}); + } +} diff --git a/src/main/yaml/type/YMerge.hx b/src/main/yaml/type/YMerge.hx new file mode 100644 index 0000000..467e462 --- /dev/null +++ b/src/main/yaml/type/YMerge.hx @@ -0,0 +1,21 @@ +package yaml.type; + +import yaml.YamlType; + +class YMerge extends yaml.StringYamlType +{ + public function new() + { + super('tag:yaml.org,2002:merge', {kind:"string"}, {skip:true}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool):String + { + return ('<<' == object) ? object : cantResolveType(); + } + + override public function represent(object:String, ?style:String):String + { + return null; + } +} diff --git a/src/main/yaml/type/YNull.hx b/src/main/yaml/type/YNull.hx new file mode 100644 index 0000000..798d9f2 --- /dev/null +++ b/src/main/yaml/type/YNull.hx @@ -0,0 +1,38 @@ +package yaml.type; + +import yaml.util.StringMap; +import yaml.YamlType; + +class YNull extends yaml.StringYamlType +{ + static var YAML_NULL_MAP:StringMap = { + var hash = new StringMap(); + hash.set('~', true); + hash.set('null', true); + hash.set('Null', true); + hash.set('NULL', true); + hash; + }; + + public function new() + { + super('tag:yaml.org,2002:null', {kind:"string"}, {kind:"null", defaultStyle:"lowercase"}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool = false):Dynamic + { + return YAML_NULL_MAP.exists(object) ? null : cantResolveType(); + } + + override public function represent(object:Dynamic, ?style:String):String + { + return switch (style) + { + case "canonical": "~"; + case "lowercase": "null"; + case "uppercase": "NULL"; + case "camelcase": "Null"; + default: "~"; + } + } +} diff --git a/src/main/yaml/type/YOmap.hx b/src/main/yaml/type/YOmap.hx new file mode 100644 index 0000000..33c7d3d --- /dev/null +++ b/src/main/yaml/type/YOmap.hx @@ -0,0 +1,76 @@ +package yaml.type; + +import yaml.util.StringMap; +import yaml.util.ObjectMap; +import Type; + +class YOmap extends yaml.YamlType, Array> +{ + public function new() + { + super("tag:yaml.org,2002:omap", {kind:"array"}, {skip:true}); + } + + // For omap this method is only doing a validation that no duplicates are present and that + // each pair has only one key. It's not really 'resolving' the value... + override public function resolve(object:Array, ?usingMaps:Bool = true, ?explicit:Bool = false):Array + { + usingMaps ? validateOMap(cast object) : validateObjectOMap(object); + return object; + } + + function validateOMap(object:Array) + { + var objectKeys = new ObjectMap(); + for (pair in object) + { + var pairHasKey = false; + var pairKey:Dynamic = null; + + if (!Std.is(pair, AnyObjectMap)) + cantResolveType(); + + for (key in pair.keys()) + { + if (pairKey == null) pairKey = key; + else cantResolveType(); // can only have one key + } + + if (pairKey == null) // must have a key + cantResolveType(); + + if (objectKeys.exists(pairKey)) + cantResolveType(); // no duplicate keys allowed + else + objectKeys.set(pairKey, null); + } + + return object; + } + + function validateObjectOMap(object:Array) + { + var objectKeys = new StringMap(); + for (pair in object) + { + var pairHasKey = false; + var pairKey:String = null; + + if (Type.typeof(pair) != ValueType.TObject) + cantResolveType(); + + for (key in Reflect.fields(pair)) + { + if (pairKey == null) pairKey = key; + else cantResolveType(); // can only have one key + } + + if (pairKey == null) // must have a key + cantResolveType(); + + + if (objectKeys.exists(pairKey)) cantResolveType(); // no duplicate keys allowed + else objectKeys.set(pairKey, null); + } + } +} diff --git a/src/main/yaml/type/YPairs.hx b/src/main/yaml/type/YPairs.hx new file mode 100644 index 0000000..9fe894d --- /dev/null +++ b/src/main/yaml/type/YPairs.hx @@ -0,0 +1,69 @@ +package yaml.type; + +import yaml.util.ObjectMap; +import yaml.YamlType; +import Type; + +class YPairs extends YamlType>, Array> +{ + public function new() + { + super("tag:yaml.org,2002:pairs", {kind:"array"}, {skip:true}); + } + + override public function resolve(object:Array, ?usingMaps:Bool = true, ?explicit:Bool = false):Array> + { + if (usingMaps) + return resolveMapPair(cast object); + else + return resolveObjectPair(object); + } + + function resolveMapPair(object:Array) + { + var result:Array> = []; + for (pair in object) + { + if (!Std.is(pair, AnyObjectMap)) + cantResolveType(); + + var fieldCount = 0; + var keyPair:Dynamic = null; + for (key in pair.keys()) + { + keyPair = key; + if (fieldCount++ > 1) break; + } + + if (fieldCount != 1) // must have one key + cantResolveType(); + + result.push([keyPair, pair.get(keyPair)]); + } + return result; + } + + function resolveObjectPair(object:Array) + { + var result:Array> = []; + for (pair in object) + { + if (Type.typeof(pair) != ValueType.TObject) + cantResolveType(); + + var fieldCount = 0; + var keyPair:Dynamic = null; + for (key in Reflect.fields(pair)) + { + keyPair = key; + if (fieldCount++ > 1) break; + } + + if (fieldCount != 1) // must have one key + cantResolveType(); + + result.push([keyPair, Reflect.field(pair, keyPair)]); + } + return result; + } +} diff --git a/src/main/yaml/type/YSeq.hx b/src/main/yaml/type/YSeq.hx new file mode 100644 index 0000000..ad30789 --- /dev/null +++ b/src/main/yaml/type/YSeq.hx @@ -0,0 +1,11 @@ +package yaml.type; + +import yaml.YamlType; + +class YSeq extends StringYamlType> +{ + public function new() + { + super('tag:yaml.org,2002:seq', {kind:"array", skip:true}, {skip:true}); + } +} diff --git a/src/main/yaml/type/YSet.hx b/src/main/yaml/type/YSet.hx new file mode 100644 index 0000000..873e634 --- /dev/null +++ b/src/main/yaml/type/YSet.hx @@ -0,0 +1,33 @@ +package yaml.type; + +import yaml.util.ObjectMap; +import yaml.YamlType; + +class YSet extends YamlType +{ + public function new() + { + super('tag:yaml.org,2002:set', {kind:"object"}, {skip:true}); + } + + override public function resolve(object:Dynamic, ?usingMaps:Bool = true, ?explicit:Bool = false):Dynamic + { + // again this is just validation, not resolving anything. + usingMaps ? validateSet(object) : validateObjectSet(object); + return object; + } + + function validateSet(object:AnyObjectMap) + { + for (key in object.keys()) + if (object.get(key) != null) + cantResolveType(); + } + + function validateObjectSet(object:Dynamic) + { + for (key in Reflect.fields(object)) + if (Reflect.field(object, key) != null) + cantResolveType(); + } +} diff --git a/src/main/yaml/type/YString.hx b/src/main/yaml/type/YString.hx new file mode 100644 index 0000000..b50d628 --- /dev/null +++ b/src/main/yaml/type/YString.hx @@ -0,0 +1,11 @@ +package yaml.type; + +import yaml.YamlType; + +class YString extends yaml.StringYamlType +{ + public function new() + { + super('tag:yaml.org,2002:str', {kind:"string", skip:true}, {skip:true}); + } +} diff --git a/src/main/yaml/type/YTimestamp.hx b/src/main/yaml/type/YTimestamp.hx new file mode 100644 index 0000000..964e6bb --- /dev/null +++ b/src/main/yaml/type/YTimestamp.hx @@ -0,0 +1,135 @@ +package yaml.type; + +import yaml.YamlType; + +//class TTimestamp extends StringYamlType +class YTimestamp extends YamlType +{ + static var YAML_TIMESTAMP_REGEXP = new EReg( + '^([0-9][0-9][0-9][0-9])' + // [1] year + '-([0-9][0-9]?)' + // [2] month + '-([0-9][0-9]?)' + // [3] day + '(?:(?:[Tt]|[ \\t]+)' + // ... + '([0-9][0-9]?)' + // [4] hour + ':([0-9][0-9])' + // [5] minute + ':([0-9][0-9])' + // [6] second + '(?:\\.([0-9]*))?' + // [7] fraction + '(?:[ \\t]*(Z|([-+])([0-9][0-9]?)' + // [8] tz [9] tz_sign [10] tz_hour + '(?::([0-9][0-9]))?))?)?$', "iu"); // [11] tz_minute + + public function new() + { + super('tag:yaml.org,2002:timestamp', {kind:"string"}, {kind:"object", instanceOf:Date}); + } + + override public function resolve(object:String, ?usingMaps:Bool = true, ?explicit:Bool = false):Date + { + if (!YAML_TIMESTAMP_REGEXP.match(object)) + cantResolveType(); + + // match: [1] year [2] month [3] day + var year:Null = 0; + var month:Null = 0; + var day:Null = 0; + var hour:Null = 0; + var minute:Null = 0; + var second:Null = 0; + var fraction:Null = 0; + var delta:Null = 0; + + try + { + year = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(1)); + month = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(2)) - 1; // month starts with 0 + day = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(3)); + hour = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(4)); + minute = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(5)); + second = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(6)); + + // EReg.matched doesn't throw exception under neko for whatever reason so we need to + // check for null on each matched call + var matched = -1; + if (year == null) matched = year = 0; + if (month == null) matched = month = 0; + if (day == null) matched = day = 0; + if (hour == null) matched = hour = 0; + if (minute == null) matched = minute = 0; + if (second == null) matched = second = 0; + + if (matched == 0) + throw "Nothing left to match"; + + var msecs = YAML_TIMESTAMP_REGEXP.matched(7); + if (msecs == null) + throw "Nothing left to match"; + + var f = msecs.substring(0, 3); + while (f.length < 3) + f += '0'; + + fraction = Std.parseInt(f); + + if (YAML_TIMESTAMP_REGEXP.matched(9) != null) + { + var tz_hour:Null = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(10)); + if (tz_hour == null) + throw "Nothing left to match"; + + var tz_minute:Null = 0; + try { + tz_minute = Std.parseInt(YAML_TIMESTAMP_REGEXP.matched(11)); + if (tz_minute == null) + tz_minute = 0; + } + catch(e:Dynamic) {} + + delta = (tz_hour * 60 + tz_minute) * 60000; // delta in mili-seconds + if ('-' == YAML_TIMESTAMP_REGEXP.matched(9)) + delta = -delta; + } + } + catch(e:Dynamic) {} + + #if (js || flash) + var stamp:Float = nativeDate().UTC(year, month, day, hour, minute, second, fraction); + + if (delta != 0) + stamp = stamp - delta; + + return Date.fromTime(stamp); + + #else + trace("Warning: UTC dates are not supported under this target"); + + var date = new Date(year, month, day, hour, minute, second); + // if (delta != 0) + // stamp = stamp - delta; + return date; + #end + } + + #if (flash || js) + static function nativeDate():Dynamic + { + #if flash9 + return untyped __global__["Date"]; + #elseif flash + return untyped _global["Date"]; + #elseif js + return untyped __js__("Date"); + #end + return null; + } + #end + + override public function represent(object:Date, ?style:String):String + { + #if (flash || js) + return yaml.util.Dates.toISOString(object); + #else + trace("Warning: UTC dates are not supported under this target"); + return object.toString(); + #end + + } +} diff --git a/src/main/yaml/util/Dates.hx b/src/main/yaml/util/Dates.hx new file mode 100644 index 0000000..60b44a5 --- /dev/null +++ b/src/main/yaml/util/Dates.hx @@ -0,0 +1,69 @@ +/* +Copyright (c) 2012 Massive Interactive + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package yaml.util; + +/** +Utility methods for working with dates +*/ +class Dates +{ + #if (flash || js) + + static function getNativeDate():Dynamic + { + #if flash9 + return untyped __global__["Date"]; + #elseif flash + return untyped _global["Date"]; + #elseif js + return untyped __js__("Date"); + #end + return null; + } + + /** + Converts a Date to UTC ISO-8601 compliant string. + + Example: 2012-06-20T12:11:08.188Z + + Currently only supported under JS, AS3 and AS2. + + @param date the date to convert to utc iso string + @return ISO-8601 compliant timestamp + */ + public static function toISOString(date:Date):String + { + var NativeDate = getNativeDate(); + var d = untyped __new__(NativeDate, date.getTime()); + + return d.getUTCFullYear() + '-' + + StringTools.lpad("" + (d.getUTCMonth() + 1), "0", 2) + '-' + + StringTools.lpad("" + d.getUTCDate(), "0", 2) + 'T' + + StringTools.lpad("" + d.getUTCHours(), "0", 2) + ':' + + StringTools.lpad("" + d.getUTCMinutes(), "0", 2) + ':' + + StringTools.lpad("" + d.getUTCSeconds(), "0", 2) + '.' + + StringTools.rpad(("" + (Floats.round(d.getUTCMilliseconds()/1000, 3))).substr(2, 5), "0", 3) + + 'Z'; + } + #end +} diff --git a/src/main/yaml/util/Floats.hx b/src/main/yaml/util/Floats.hx new file mode 100644 index 0000000..43d09e0 --- /dev/null +++ b/src/main/yaml/util/Floats.hx @@ -0,0 +1,69 @@ +/* +Copyright (c) 2012 Massive Interactive + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package yaml.util; + +/** +Utility methods for working with float values. +*/ +class Floats +{ + /** + Returns the string representation of a float. + + Under cpp will convert large floats with a positive exponent (eg date.getTime()) to a decimal string. + e.g 1.347420344e+12 -> 1347420344000 + */ + public static function toString(value:Float):String + { + #if cpp + var str = Std.string(value); + if (str.indexOf("e+") != -1) + { + var parts = str.split("."); + var exp = parts[1].split("e+"); + str = parts[0] + StringTools.rpad(exp[0], "0", Std.parseInt(exp[1])); + } + return str; + #else + return Std.string(value); + #end + } + + /** + Rounds a value to a specific decimal precision + + Example: +
+	FloatUtil.round(1.6666667 3);//returns 1.667;
+	
+ + @param value value to round + @param precision precision in decimals + @return the rounded value + */ + static public function round(value:Float, precision:Int):Float + { + value = value * Math.pow(10, precision); + return Math.round(value) / Math.pow(10, precision); + } +} diff --git a/src/main/yaml/util/IntMap.hx b/src/main/yaml/util/IntMap.hx new file mode 100644 index 0000000..12bc00b --- /dev/null +++ b/src/main/yaml/util/IntMap.hx @@ -0,0 +1,7 @@ +package yaml.util; + +#if haxe3 +typedef IntMap = haxe.ds.IntMap; +#else +typedef IntMap = IntHash; +#end diff --git a/src/main/yaml/util/Ints.hx b/src/main/yaml/util/Ints.hx new file mode 100644 index 0000000..a11490a --- /dev/null +++ b/src/main/yaml/util/Ints.hx @@ -0,0 +1,167 @@ +/* +Copyright (c) 2012 Massive Interactive + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package yaml.util; + +/** +Utility methods for working with int values. +*/ +class Ints +{ + // Base used for toString/parseInt conversions. Supporting base 2 to 36 for now as common standard. + static var BASE = "0123456789abcdefghijklmnopqrstuvwxyz"; + + /** + Returns the string representation of an integer value based on an optional radix (base). + + A default base of 10 is used if none is supplied. + + A base below 2 or above 36 will cause an exception. + + @param value The integer value to return the string representation of. + @param radix An integer between 2 and 36 to be used as the base for conversion. Defaults to 10. + @return The string representation of the integer at the defined radix. + */ + public static function toString(value:Int, ?radix:Int = 10):String + { + if (radix < 2 || radix > BASE.length) + throw "Unsupported radix " + radix; + + #if (js || flash) + + return untyped value.toString(radix); + + #else + + if (radix == 10) return Std.string(value); + else if (value == 0) return '0'; + else // do the conversion ourselves + { + var sign = ""; + if (value < 0) + { + sign = '-'; + value = -value; + } + + var result = ''; + while (value > 0) + { + result = BASE.charAt(value % radix) + result; + value = Std.int(value / radix); + } + + return sign + result; + } + + #end + } + + /** + Parses a string and returns the decimal integer value it represents or `null` if no value could be parsed. + + An optional radix can be supplied to indicate the base the value should be parsed under. + If no base is supplied a base of 10 will be assumed, unless the value begins with '0x', in which case + a base of 16 (hex) is assumed. + + Parsing continues until complete or an invalid char is found. In that case the value parsed up until that point + is returned. e.g. `parseInt("44dae", 10)` returns `44`. + + The reason for returning `null` instead of `Math.NaN` when parsing fails completely is that NaN is a Float which + would force the return type to also be Float. This would mean `var a:Int = parseInt(value)` would become invalid, + which is unintuitive. A return type of Dynamic would fix this on most systems, but not flash (avm2) which + will auto cast the Float to an Int meaning Math.isNaN test would always fail. + + @param value The value to parse into an integer + @param radix The radix to base the conversion on. Default is 16 for string starting with '0x' otherwise 10. + @return The decimal integer value which the supplied string represents, or null if it could not be parsed. + */ + public static function parseInt(value:String, ?radix:Null):Null + { + if (radix != null && (radix < 2 || radix > BASE.length)) + throw "Unsupported radix " + radix; + + #if js + + var v = untyped __js__("parseInt")(value, radix); + return (untyped __js__("isNaN")(v)) ? null : v; + + #elseif flash9 + + if (radix == null) radix = 0; + var v = untyped __global__["parseInt"](value, radix); + return (untyped __global__["isNaN"](v)) ? null : v; + + #elseif flash8 + + var v = _global["parseInt"](value, radix); + return _global["isNaN"](v) ? null : v; + + #else // do the conversion ourselves + + value = StringTools.trim(value).toLowerCase(); + + if (value.length == 0) + return null; + + var s = 0; + var neg = false; + if (value.indexOf("-") == 0) + { + neg = true; + s = 1; + } + else if (value.indexOf("+") == 0) + { + s = 1; + } + + if (s == 1 && value.length == 1) + return null; + + var j = value.indexOf("0x"); + if ((j == 0 || (j == 1 && s == 1)) && (radix == null || radix == 16)) + { + s += 2; + if (radix == null) radix = 16; + } + else if (radix == null) + { + radix = 10; + } + + var result = 0; + for (i in s...value.length) + { + var x = BASE.indexOf(value.charAt(i)); + if (x == -1 || x >= radix) + { + if (i == s) return null; + else return neg ? -result : result; + } + result = (result * radix) + x; + } + + return neg ? -result : result; + #end + } +} diff --git a/src/main/yaml/util/ObjectMap.hx b/src/main/yaml/util/ObjectMap.hx new file mode 100644 index 0000000..7f10afe --- /dev/null +++ b/src/main/yaml/util/ObjectMap.hx @@ -0,0 +1,103 @@ +package yaml.util; + +/// Wanted to use haxe.ds.ObjectMap under haxe3 but it doesn't support primitive keys on all targets so +/// we'll use our own TObjectMap instead. + +//#if haxe3 +//under haxe 3 use built in map for speed, even though this prevents us using a null key. +//typedef ObjectMap = haxe.ds.ObjectMap; +//typedef AnyObjectMap = ObjectMap<{}, Dynamic>; +//#else +typedef ObjectMap = TObjectMap; +typedef AnyObjectMap = ObjectMap; + +/** +Cross platform object map which also supports the option of one null key. +*/ +class TObjectMap +{ + var _keys:Array; + var values:Array; + + public function new(?weakKeys:Bool = false) + { + _keys = []; + values = []; + } + + public function set(key:K, value:V):Void + { + for (i in 0..._keys.length) + { + if (_keys[i] == key) + { + _keys[i] = key; + values[i] = value; + return; + } + } + _keys.push(key); + values.push(value); + } + + public function get(key:K):Null + { + for (i in 0..._keys.length) + { + if (_keys[i] == key) + return values[i]; + } + return null; + } + + public function exists(key:K):Bool + { + for (k in _keys) + if (k == key) + return true; + return false; + } + + public function remove(key:K):Bool + { + for (i in 0..._keys.length) + { + if (_keys[i] == key) + { + _keys.splice(i, 1); + values.splice(i, 1); + return true; + } + } + return false; + } + + public function keys():Iterator + { + return _keys.iterator(); + } + + public function iterator():Iterator + { + return values.iterator(); + } + + public function toString():String + { + var s = "{"; + var ks:Dynamic = _keys; + var vs:Dynamic = values; + for (i in 0..._keys.length) + { + var k = (Std.is(ks[i], Array)) ? "[" + ks[i] + "]" : ks[i]; + var v = (Std.is(vs[i], Array)) ? "[" + vs[i] + "]" : vs[i]; + s += k + " => " + v + ", "; + } + + if (_keys.length > 0) + s = s.substr(0, s.length - 2); + + return s + "}"; + } +} +//#end diff --git a/src/main/yaml/util/StringMap.hx b/src/main/yaml/util/StringMap.hx new file mode 100644 index 0000000..69962a4 --- /dev/null +++ b/src/main/yaml/util/StringMap.hx @@ -0,0 +1,7 @@ +package yaml.util; + +#if haxe3 +typedef StringMap = haxe.ds.StringMap; +#else +typedef StringMap = Hash; +#end diff --git a/src/main/yaml/util/Strings.hx b/src/main/yaml/util/Strings.hx new file mode 100644 index 0000000..cccdbb5 --- /dev/null +++ b/src/main/yaml/util/Strings.hx @@ -0,0 +1,40 @@ +/* +Copyright (c) 2012 Massive Interactive + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package yaml.util; + +/** +Utility methods for working with strings. +*/ +class Strings +{ + /** + Concatenate a source string with itself N number of times. + */ + public static function repeat(source:String, times:Int):String + { + var result = ""; + for (i in 0...times) + result += source; + return result; + } +} diff --git a/src/main/yaml/util/Utf8.hx b/src/main/yaml/util/Utf8.hx new file mode 100644 index 0000000..07df117 --- /dev/null +++ b/src/main/yaml/util/Utf8.hx @@ -0,0 +1,27 @@ +package yaml.util; + +class Utf8 +{ + public static function substring(value:String, startIndex:Int, ?endIndex:Null):String + { + var size = haxe.Utf8.length(value); + var pos = startIndex; + var length = 0; + + if (endIndex == null) + { + length = size - pos; + } + else + { + if (startIndex > endIndex) + { + pos = endIndex; + endIndex = startIndex; + } + length = endIndex - pos; + } + + return haxe.Utf8.sub(value, pos, length); + } +}