You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
657 lines
15 KiB
657 lines
15 KiB
/**
|
|
* Copyright (c) 2015-present, Horcrux.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the MIT-style license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RNSVGPathParser.h"
|
|
#import <React/RCTLog.h>
|
|
#import <math.h>
|
|
#import "RNSVGBezierElement.h"
|
|
|
|
@implementation RNSVGPathParser {
|
|
char prev_cmd;
|
|
NSUInteger i;
|
|
NSUInteger l;
|
|
NSString *s;
|
|
float _penX;
|
|
float _penY;
|
|
float _penDownX;
|
|
float _penDownY;
|
|
float _pivotX;
|
|
float _pivotY;
|
|
BOOL _valid;
|
|
BOOL _penDownSet;
|
|
}
|
|
|
|
- (instancetype)initWithPathString:(NSString *)d
|
|
{
|
|
if (self = [super init]) {
|
|
prev_cmd = ' ';
|
|
l = [d length];
|
|
i = 0;
|
|
s = d;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#define NEXT_FLOAT [self parse_list_number]
|
|
#define NEXT_BOOL [self parse_flag]
|
|
|
|
- (CGPathRef)getPath
|
|
{
|
|
CGMutablePathRef path = CGPathCreateMutable();
|
|
while (i < l) {
|
|
[self skip_spaces];
|
|
|
|
if (i >= l) {
|
|
break;
|
|
}
|
|
|
|
bool has_prev_cmd = prev_cmd != ' ';
|
|
char first_char = [s characterAtIndex:i];
|
|
|
|
if (!has_prev_cmd && first_char != 'M' && first_char != 'm') {
|
|
// The first segment must be a MoveTo.
|
|
RCTLogError(@"UnexpectedData: %@", s);
|
|
CGPathRelease(path);
|
|
return nil;
|
|
}
|
|
|
|
// TODO: simplify
|
|
bool is_implicit_move_to = false;
|
|
char cmd = ' ';
|
|
if ([self is_cmd:first_char]) {
|
|
is_implicit_move_to = false;
|
|
cmd = first_char;
|
|
i += 1;
|
|
} else if ([self is_number_start:first_char] && has_prev_cmd) {
|
|
if (prev_cmd == 'Z' || prev_cmd == 'z') {
|
|
// ClosePath cannot be followed by a number.
|
|
RCTLogError(@"UnexpectedData: %@", s);
|
|
CGPathRelease(path);
|
|
return nil;
|
|
}
|
|
|
|
if (prev_cmd == 'M' || prev_cmd == 'm') {
|
|
// 'If a moveto is followed by multiple pairs of coordinates,
|
|
// the subsequent pairs are treated as implicit lineto commands.'
|
|
// So we parse them as LineTo.
|
|
is_implicit_move_to = true;
|
|
if ([self is_absolute:prev_cmd]) {
|
|
cmd = 'L';
|
|
} else {
|
|
cmd = 'l';
|
|
}
|
|
} else {
|
|
is_implicit_move_to = false;
|
|
cmd = prev_cmd;
|
|
}
|
|
} else {
|
|
RCTLogError(@"UnexpectedData: %@", s);
|
|
CGPathRelease(path);
|
|
return nil;
|
|
}
|
|
|
|
bool absolute = [self is_absolute:cmd];
|
|
switch (cmd) {
|
|
case 'm': {
|
|
[self move:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'M': {
|
|
[self moveTo:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'l': {
|
|
[self line:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'L': {
|
|
[self lineTo:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'h': {
|
|
[self line:path x:NEXT_FLOAT y:0];
|
|
break;
|
|
}
|
|
case 'H': {
|
|
[self lineTo:path x:NEXT_FLOAT y:_penY];
|
|
break;
|
|
}
|
|
case 'v': {
|
|
[self line:path x:0 y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'V': {
|
|
[self lineTo:path x:_penX y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'c': {
|
|
[self curve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'C': {
|
|
[self curveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 's': {
|
|
[self smoothCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'S': {
|
|
[self smoothCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'q': {
|
|
[self quadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'Q': {
|
|
[self quadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 't': {
|
|
[self smoothQuadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'T': {
|
|
[self smoothQuadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'a': {
|
|
[self arc:path
|
|
rx:NEXT_FLOAT
|
|
ry:NEXT_FLOAT
|
|
rotation:NEXT_FLOAT
|
|
outer:NEXT_BOOL
|
|
clockwise:NEXT_BOOL
|
|
x:NEXT_FLOAT
|
|
y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'A': {
|
|
[self arcTo:path
|
|
rx:NEXT_FLOAT
|
|
ry:NEXT_FLOAT
|
|
rotation:NEXT_FLOAT
|
|
outer:NEXT_BOOL
|
|
clockwise:NEXT_BOOL
|
|
x:NEXT_FLOAT
|
|
y:NEXT_FLOAT];
|
|
break;
|
|
}
|
|
case 'z':
|
|
case 'Z': {
|
|
[self close:path];
|
|
break;
|
|
}
|
|
default: {
|
|
RCTLogError(@"UnexpectedData: %@", s);
|
|
CGPathRelease(path);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
if (is_implicit_move_to) {
|
|
if (absolute) {
|
|
prev_cmd = 'M';
|
|
} else {
|
|
prev_cmd = 'm';
|
|
}
|
|
} else {
|
|
prev_cmd = cmd;
|
|
}
|
|
}
|
|
|
|
return (CGPathRef)CFAutorelease(path);
|
|
}
|
|
|
|
- (void)move:(CGMutablePathRef)path x:(float)x y:(float)y
|
|
{
|
|
[self moveTo:path x:x + _penX y:y + _penY];
|
|
}
|
|
|
|
- (void)moveTo:(CGMutablePathRef)path x:(float)x y:(float)y
|
|
{
|
|
// RCTLogInfo(@"move x: %f y: %f", x, y);
|
|
_penDownX = _pivotX = _penX = x;
|
|
_penDownY = _pivotY = _penY = y;
|
|
CGPathMoveToPoint(path, nil, x, y);
|
|
}
|
|
|
|
- (void)line:(CGMutablePathRef)path x:(float)x y:(float)y
|
|
{
|
|
[self lineTo:path x:x + _penX y:y + _penY];
|
|
}
|
|
|
|
- (void)lineTo:(CGMutablePathRef)path x:(float)x y:(float)y
|
|
{
|
|
// RCTLogInfo(@"line x: %f y: %f", x, y);
|
|
[self setPenDown];
|
|
_pivotX = _penX = x;
|
|
_pivotY = _penY = y;
|
|
CGPathAddLineToPoint(path, nil, x, y);
|
|
}
|
|
|
|
- (void)curve:(CGMutablePathRef)path
|
|
c1x:(float)c1x
|
|
c1y:(float)c1y
|
|
c2x:(float)c2x
|
|
c2y:(float)c2y
|
|
ex:(float)ex
|
|
ey:(float)ey
|
|
{
|
|
[self curveTo:path c1x:c1x + _penX c1y:c1y + _penY c2x:c2x + _penX c2y:c2y + _penY ex:ex + _penX ey:ey + _penY];
|
|
}
|
|
|
|
- (void)curveTo:(CGMutablePathRef)path
|
|
c1x:(float)c1x
|
|
c1y:(float)c1y
|
|
c2x:(float)c2x
|
|
c2y:(float)c2y
|
|
ex:(float)ex
|
|
ey:(float)ey
|
|
{
|
|
// RCTLogInfo(@"curve c1x: %f c1y: %f c2x: %f c2y: %f ex: %f ey: %f", c1x, c1y, c2x, c2y, ex, ey);
|
|
_pivotX = c2x;
|
|
_pivotY = c2y;
|
|
[self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey];
|
|
}
|
|
|
|
- (void)curveToPoint:(CGMutablePathRef)path
|
|
c1x:(float)c1x
|
|
c1y:(float)c1y
|
|
c2x:(float)c2x
|
|
c2y:(float)c2y
|
|
ex:(float)ex
|
|
ey:(float)ey
|
|
{
|
|
[self setPenDown];
|
|
_penX = ex;
|
|
_penY = ey;
|
|
CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey);
|
|
}
|
|
|
|
- (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey
|
|
{
|
|
[self smoothCurveTo:path c1x:c1x + _penX c1y:c1y + _penY ex:ex + _penX ey:ey + _penY];
|
|
}
|
|
|
|
- (void)smoothCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey
|
|
{
|
|
// RCTLogInfo(@"smoothcurve c1x: %f c1y: %f ex: %f ey: %f", c1x, c1y, ex, ey);
|
|
float c2x = c1x;
|
|
float c2y = c1y;
|
|
c1x = (_penX * 2) - _pivotX;
|
|
c1y = (_penY * 2) - _pivotY;
|
|
_pivotX = c2x;
|
|
_pivotY = c2y;
|
|
[self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey];
|
|
}
|
|
|
|
- (void)quadraticBezierCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y
|
|
{
|
|
[self quadraticBezierCurveTo:path
|
|
c1x:(float)c1x + _penX
|
|
c1y:(float)c1y + _penY
|
|
c2x:(float)c2x + _penX
|
|
c2y:(float)c2y + _penY];
|
|
}
|
|
|
|
- (void)quadraticBezierCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y
|
|
{
|
|
// RCTLogInfo(@"quad c1x: %f c1y: %f c2x: %f c2y: %f", c1x, c1y, c2x, c2y);
|
|
_pivotX = c1x;
|
|
_pivotY = c1y;
|
|
float ex = c2x;
|
|
float ey = c2y;
|
|
c2x = (ex + c1x * 2) / 3;
|
|
c2y = (ey + c1y * 2) / 3;
|
|
c1x = (_penX + c1x * 2) / 3;
|
|
c1y = (_penY + c1y * 2) / 3;
|
|
[self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey];
|
|
}
|
|
|
|
- (void)smoothQuadraticBezierCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y
|
|
{
|
|
[self smoothQuadraticBezierCurveTo:path c1x:c1x + _penX c1y:c1y + _penY];
|
|
}
|
|
|
|
- (void)smoothQuadraticBezierCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y
|
|
{
|
|
// RCTLogInfo(@"smoothquad c1x: %f c1y: %f", c1x, c1y);
|
|
float c2x = c1x;
|
|
float c2y = c1y;
|
|
c1x = (_penX * 2) - _pivotX;
|
|
c1y = (_penY * 2) - _pivotY;
|
|
[self quadraticBezierCurveTo:path c1x:c1x c1y:c1y c2x:c2x c2y:c2y];
|
|
}
|
|
|
|
- (void)arc:(CGMutablePathRef)path
|
|
rx:(float)rx
|
|
ry:(float)ry
|
|
rotation:(float)rotation
|
|
outer:(BOOL)outer
|
|
clockwise:(BOOL)clockwise
|
|
x:(float)x
|
|
y:(float)y
|
|
{
|
|
[self arcTo:path rx:rx ry:ry rotation:rotation outer:outer clockwise:clockwise x:x + _penX y:y + _penY];
|
|
}
|
|
|
|
- (void)arcTo:(CGMutablePathRef)path
|
|
rx:(float)rx
|
|
ry:(float)ry
|
|
rotation:(float)rotation
|
|
outer:(BOOL)outer
|
|
clockwise:(BOOL)clockwise
|
|
x:(float)x
|
|
y:(float)y
|
|
{
|
|
// RCTLogInfo(@"arc rx: %f ry: %f rotation: %f outer: %i clockwise: %i x: %f y: %f", rx, ry, rotation, outer,
|
|
// clockwise, x, y);
|
|
float tX = _penX;
|
|
float tY = _penY;
|
|
|
|
ry = fabsf(ry == 0 ? (rx == 0 ? (y - tY) : rx) : ry);
|
|
rx = fabsf(rx == 0 ? (x - tX) : rx);
|
|
|
|
if (rx == 0 || ry == 0 || (x == tX && y == tY)) {
|
|
[self lineTo:path x:x y:y];
|
|
return;
|
|
}
|
|
|
|
float rad = rotation * (float)M_PI / 180;
|
|
float cosed = cosf(rad);
|
|
float sined = sinf(rad);
|
|
x -= tX;
|
|
y -= tY;
|
|
// Ellipse Center
|
|
float cx = cosed * x / 2 + sined * y / 2;
|
|
float cy = -sined * x / 2 + cosed * y / 2;
|
|
float rxry = rx * rx * ry * ry;
|
|
float rycx = ry * ry * cx * cx;
|
|
float rxcy = rx * rx * cy * cy;
|
|
float a = rxry - rxcy - rycx;
|
|
|
|
if (a < 0) {
|
|
a = sqrtf(1 - a / rxry);
|
|
rx *= a;
|
|
ry *= a;
|
|
cx = x / 2;
|
|
cy = y / 2;
|
|
} else {
|
|
a = sqrtf(a / (rxcy + rycx));
|
|
|
|
if (outer == clockwise) {
|
|
a = -a;
|
|
}
|
|
float cxd = -a * cy * rx / ry;
|
|
float cyd = a * cx * ry / rx;
|
|
cx = cosed * cxd - sined * cyd + x / 2;
|
|
cy = sined * cxd + cosed * cyd + y / 2;
|
|
}
|
|
|
|
// Rotation + Scale Transform
|
|
float xx = cosed / rx;
|
|
float yx = sined / rx;
|
|
float xy = -sined / ry;
|
|
float yy = cosed / ry;
|
|
|
|
// Start and End Angle
|
|
float sa = atan2f(xy * -cx + yy * -cy, xx * -cx + yx * -cy);
|
|
float ea = atan2f(xy * (x - cx) + yy * (y - cy), xx * (x - cx) + yx * (y - cy));
|
|
|
|
cx += tX;
|
|
cy += tY;
|
|
x += tX;
|
|
y += tY;
|
|
|
|
[self setPenDown];
|
|
|
|
_penX = _pivotX = x;
|
|
_penY = _pivotY = y;
|
|
|
|
[self arcToBezier:path cx:cx cy:cy rx:rx ry:ry sa:sa ea:ea clockwise:clockwise rad:rad];
|
|
}
|
|
|
|
- (void)arcToBezier:(CGMutablePathRef)path
|
|
cx:(float)cx
|
|
cy:(float)cy
|
|
rx:(float)rx
|
|
ry:(float)ry
|
|
sa:(float)sa
|
|
ea:(float)ea
|
|
clockwise:(BOOL)clockwise
|
|
rad:(float)rad
|
|
{
|
|
// Inverse Rotation + Scale Transform
|
|
float cosed = cosf(rad);
|
|
float sined = sinf(rad);
|
|
float xx = cosed * rx;
|
|
float yx = -sined * ry;
|
|
float xy = sined * rx;
|
|
float yy = cosed * ry;
|
|
|
|
// Bezier Curve Approximation
|
|
float arc = ea - sa;
|
|
if (arc < 0 && clockwise) {
|
|
arc += M_PI * 2;
|
|
} else if (arc > 0 && !clockwise) {
|
|
arc -= M_PI * 2;
|
|
}
|
|
|
|
int n = (int)ceilf(fabsf(arc / ((float)M_PI / 2)));
|
|
|
|
float step = arc / n;
|
|
float k = (4.0f / 3.0f) * tanf(step / 4);
|
|
|
|
float x = cosf(sa);
|
|
float y = sinf(sa);
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
float cp1x = x - k * y;
|
|
float cp1y = y + k * x;
|
|
|
|
sa += step;
|
|
x = cosf(sa);
|
|
y = sinf(sa);
|
|
|
|
float cp2x = x + k * y;
|
|
float cp2y = y - k * x;
|
|
|
|
float c1x = cx + xx * cp1x + yx * cp1y;
|
|
float c1y = cy + xy * cp1x + yy * cp1y;
|
|
float c2x = cx + xx * cp2x + yx * cp2y;
|
|
float c2y = cy + xy * cp2x + yy * cp2y;
|
|
float ex = cx + xx * x + yx * y;
|
|
float ey = cy + xy * x + yy * y;
|
|
CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey);
|
|
}
|
|
}
|
|
|
|
- (void)close:(CGMutablePathRef)path
|
|
{
|
|
if (_penDownSet) {
|
|
_penX = _penDownX;
|
|
_penY = _penDownY;
|
|
_penDownSet = NO;
|
|
CGPathCloseSubpath(path);
|
|
}
|
|
}
|
|
|
|
- (void)setPenDown
|
|
{
|
|
if (!_penDownSet) {
|
|
_penDownX = _penX;
|
|
_penDownY = _penY;
|
|
_penDownSet = YES;
|
|
}
|
|
}
|
|
|
|
- (void)skip_spaces
|
|
{
|
|
while (i < l && [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[s characterAtIndex:i]])
|
|
i++;
|
|
}
|
|
|
|
- (bool)is_cmd:(char)c
|
|
{
|
|
switch (c) {
|
|
case 'M':
|
|
case 'm':
|
|
case 'Z':
|
|
case 'z':
|
|
case 'L':
|
|
case 'l':
|
|
case 'H':
|
|
case 'h':
|
|
case 'V':
|
|
case 'v':
|
|
case 'C':
|
|
case 'c':
|
|
case 'S':
|
|
case 's':
|
|
case 'Q':
|
|
case 'q':
|
|
case 'T':
|
|
case 't':
|
|
case 'A':
|
|
case 'a':
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
- (bool)is_number_start:(char)c
|
|
{
|
|
return (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+';
|
|
}
|
|
|
|
- (bool)is_absolute:(char)c
|
|
{
|
|
return [[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:c];
|
|
}
|
|
|
|
// By the SVG spec 'large-arc' and 'sweep' must contain only one char
|
|
// and can be written without any separators, e.g.: 10 20 30 01 10 20.
|
|
- (bool)parse_flag
|
|
{
|
|
[self skip_spaces];
|
|
|
|
char c = [s characterAtIndex:i];
|
|
switch (c) {
|
|
case '0':
|
|
case '1': {
|
|
i += 1;
|
|
if (i < l && [s characterAtIndex:i] == ',') {
|
|
i += 1;
|
|
}
|
|
[self skip_spaces];
|
|
break;
|
|
}
|
|
default:
|
|
RCTLogError(@"UnexpectedData: %@", s);
|
|
}
|
|
|
|
return c == '1';
|
|
}
|
|
|
|
- (float)parse_list_number
|
|
{
|
|
if (i == l) {
|
|
RCTLogError(@"UnexpectedEnd: %@", s);
|
|
}
|
|
|
|
float n = [self parse_number];
|
|
[self skip_spaces];
|
|
[self parse_list_separator];
|
|
|
|
return n;
|
|
}
|
|
|
|
- (float)parse_number
|
|
{
|
|
// Strip off leading whitespaces.
|
|
[self skip_spaces];
|
|
|
|
if (i == l) {
|
|
RCTLogError(@"InvalidNumber: %@", s);
|
|
}
|
|
|
|
NSUInteger start = i;
|
|
|
|
char c = [s characterAtIndex:i];
|
|
|
|
// Consume sign.
|
|
if (c == '-' || c == '+') {
|
|
i += 1;
|
|
c = [s characterAtIndex:i];
|
|
}
|
|
|
|
// Consume integer.
|
|
if (c >= '0' && c <= '9') {
|
|
[self skip_digits];
|
|
if (i < l) {
|
|
c = [s characterAtIndex:i];
|
|
}
|
|
} else if (c != '.') {
|
|
RCTLogError(@"InvalidNumber: %@", s);
|
|
}
|
|
|
|
// Consume fraction.
|
|
if (c == '.') {
|
|
i += 1;
|
|
[self skip_digits];
|
|
if (i < l) {
|
|
c = [s characterAtIndex:i];
|
|
}
|
|
}
|
|
|
|
if ((c == 'e' || c == 'E') && i + 1 < l) {
|
|
char c2 = [s characterAtIndex:i + 1];
|
|
// Check for `em`/`ex`.
|
|
if (c2 != 'm' && c2 != 'x') {
|
|
i += 1;
|
|
c = [s characterAtIndex:i];
|
|
|
|
if (c == '+' || c == '-') {
|
|
i += 1;
|
|
[self skip_digits];
|
|
} else if (c >= '0' && c <= '9') {
|
|
[self skip_digits];
|
|
} else {
|
|
RCTLogError(@"InvalidNumber: %@", s);
|
|
}
|
|
}
|
|
}
|
|
|
|
NSString *num = [s substringWithRange:NSMakeRange(start, i - start)];
|
|
float n = [num floatValue];
|
|
|
|
// inf, nan, etc. are an error.
|
|
if (!isfinite(n)) {
|
|
RCTLogError(@"InvalidNumber: %@", s);
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
- (void)parse_list_separator
|
|
{
|
|
if (i < l && [s characterAtIndex:i] == ',') {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
- (void)skip_digits
|
|
{
|
|
while (i < l && [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i]])
|
|
i++;
|
|
}
|
|
|
|
@end
|