URL
https://opencores.org/ocsvn/openrisc/openrisc/trunk
Subversion Repositories openrisc
Compare Revisions
- This comparison shows the changes necessary to convert path
/openrisc/tags/gnu-dev/fsf-gcc-snapshot-1-mar-12/or1k-gcc/libgo/go/html
- from Rev 747 to Rev 783
- ↔ Reverse comparison
Rev 747 → Rev 783
/entity_test.go
0,0 → 1,29
// Copyright 2010 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package html |
|
import ( |
"testing" |
"unicode/utf8" |
) |
|
func TestEntityLength(t *testing.T) { |
// We verify that the length of UTF-8 encoding of each value is <= 1 + len(key). |
// The +1 comes from the leading "&". This property implies that the length of |
// unescaped text is <= the length of escaped text. |
for k, v := range entity { |
if 1+len(k) < utf8.RuneLen(v) { |
t.Error("escaped entity &" + k + " is shorter than its UTF-8 encoding " + string(v)) |
} |
if len(k) > longestEntityWithoutSemicolon && k[len(k)-1] != ';' { |
t.Errorf("entity name %s is %d characters, but longestEntityWithoutSemicolon=%d", k, len(k), longestEntityWithoutSemicolon) |
} |
} |
for k, v := range entity2 { |
if 1+len(k) < utf8.RuneLen(v[0])+utf8.RuneLen(v[1]) { |
t.Error("escaped entity &" + k + " is shorter than its UTF-8 encoding " + string(v[0]) + string(v[1])) |
} |
} |
} |
/entity.go
0,0 → 1,2253
// Copyright 2010 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package html |
|
// All entities that do not end with ';' are 6 or fewer bytes long. |
const longestEntityWithoutSemicolon = 6 |
|
// entity is a map from HTML entity names to their values. The semicolon matters: |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/named-character-references.html |
// lists both "amp" and "amp;" as two separate entries. |
// |
// Note that the HTML5 list is larger than the HTML4 list at |
// http://www.w3.org/TR/html4/sgml/entities.html |
var entity = map[string]rune{ |
"AElig;": '\U000000C6', |
"AMP;": '\U00000026', |
"Aacute;": '\U000000C1', |
"Abreve;": '\U00000102', |
"Acirc;": '\U000000C2', |
"Acy;": '\U00000410', |
"Afr;": '\U0001D504', |
"Agrave;": '\U000000C0', |
"Alpha;": '\U00000391', |
"Amacr;": '\U00000100', |
"And;": '\U00002A53', |
"Aogon;": '\U00000104', |
"Aopf;": '\U0001D538', |
"ApplyFunction;": '\U00002061', |
"Aring;": '\U000000C5', |
"Ascr;": '\U0001D49C', |
"Assign;": '\U00002254', |
"Atilde;": '\U000000C3', |
"Auml;": '\U000000C4', |
"Backslash;": '\U00002216', |
"Barv;": '\U00002AE7', |
"Barwed;": '\U00002306', |
"Bcy;": '\U00000411', |
"Because;": '\U00002235', |
"Bernoullis;": '\U0000212C', |
"Beta;": '\U00000392', |
"Bfr;": '\U0001D505', |
"Bopf;": '\U0001D539', |
"Breve;": '\U000002D8', |
"Bscr;": '\U0000212C', |
"Bumpeq;": '\U0000224E', |
"CHcy;": '\U00000427', |
"COPY;": '\U000000A9', |
"Cacute;": '\U00000106', |
"Cap;": '\U000022D2', |
"CapitalDifferentialD;": '\U00002145', |
"Cayleys;": '\U0000212D', |
"Ccaron;": '\U0000010C', |
"Ccedil;": '\U000000C7', |
"Ccirc;": '\U00000108', |
"Cconint;": '\U00002230', |
"Cdot;": '\U0000010A', |
"Cedilla;": '\U000000B8', |
"CenterDot;": '\U000000B7', |
"Cfr;": '\U0000212D', |
"Chi;": '\U000003A7', |
"CircleDot;": '\U00002299', |
"CircleMinus;": '\U00002296', |
"CirclePlus;": '\U00002295', |
"CircleTimes;": '\U00002297', |
"ClockwiseContourIntegral;": '\U00002232', |
"CloseCurlyDoubleQuote;": '\U0000201D', |
"CloseCurlyQuote;": '\U00002019', |
"Colon;": '\U00002237', |
"Colone;": '\U00002A74', |
"Congruent;": '\U00002261', |
"Conint;": '\U0000222F', |
"ContourIntegral;": '\U0000222E', |
"Copf;": '\U00002102', |
"Coproduct;": '\U00002210', |
"CounterClockwiseContourIntegral;": '\U00002233', |
"Cross;": '\U00002A2F', |
"Cscr;": '\U0001D49E', |
"Cup;": '\U000022D3', |
"CupCap;": '\U0000224D', |
"DD;": '\U00002145', |
"DDotrahd;": '\U00002911', |
"DJcy;": '\U00000402', |
"DScy;": '\U00000405', |
"DZcy;": '\U0000040F', |
"Dagger;": '\U00002021', |
"Darr;": '\U000021A1', |
"Dashv;": '\U00002AE4', |
"Dcaron;": '\U0000010E', |
"Dcy;": '\U00000414', |
"Del;": '\U00002207', |
"Delta;": '\U00000394', |
"Dfr;": '\U0001D507', |
"DiacriticalAcute;": '\U000000B4', |
"DiacriticalDot;": '\U000002D9', |
"DiacriticalDoubleAcute;": '\U000002DD', |
"DiacriticalGrave;": '\U00000060', |
"DiacriticalTilde;": '\U000002DC', |
"Diamond;": '\U000022C4', |
"DifferentialD;": '\U00002146', |
"Dopf;": '\U0001D53B', |
"Dot;": '\U000000A8', |
"DotDot;": '\U000020DC', |
"DotEqual;": '\U00002250', |
"DoubleContourIntegral;": '\U0000222F', |
"DoubleDot;": '\U000000A8', |
"DoubleDownArrow;": '\U000021D3', |
"DoubleLeftArrow;": '\U000021D0', |
"DoubleLeftRightArrow;": '\U000021D4', |
"DoubleLeftTee;": '\U00002AE4', |
"DoubleLongLeftArrow;": '\U000027F8', |
"DoubleLongLeftRightArrow;": '\U000027FA', |
"DoubleLongRightArrow;": '\U000027F9', |
"DoubleRightArrow;": '\U000021D2', |
"DoubleRightTee;": '\U000022A8', |
"DoubleUpArrow;": '\U000021D1', |
"DoubleUpDownArrow;": '\U000021D5', |
"DoubleVerticalBar;": '\U00002225', |
"DownArrow;": '\U00002193', |
"DownArrowBar;": '\U00002913', |
"DownArrowUpArrow;": '\U000021F5', |
"DownBreve;": '\U00000311', |
"DownLeftRightVector;": '\U00002950', |
"DownLeftTeeVector;": '\U0000295E', |
"DownLeftVector;": '\U000021BD', |
"DownLeftVectorBar;": '\U00002956', |
"DownRightTeeVector;": '\U0000295F', |
"DownRightVector;": '\U000021C1', |
"DownRightVectorBar;": '\U00002957', |
"DownTee;": '\U000022A4', |
"DownTeeArrow;": '\U000021A7', |
"Downarrow;": '\U000021D3', |
"Dscr;": '\U0001D49F', |
"Dstrok;": '\U00000110', |
"ENG;": '\U0000014A', |
"ETH;": '\U000000D0', |
"Eacute;": '\U000000C9', |
"Ecaron;": '\U0000011A', |
"Ecirc;": '\U000000CA', |
"Ecy;": '\U0000042D', |
"Edot;": '\U00000116', |
"Efr;": '\U0001D508', |
"Egrave;": '\U000000C8', |
"Element;": '\U00002208', |
"Emacr;": '\U00000112', |
"EmptySmallSquare;": '\U000025FB', |
"EmptyVerySmallSquare;": '\U000025AB', |
"Eogon;": '\U00000118', |
"Eopf;": '\U0001D53C', |
"Epsilon;": '\U00000395', |
"Equal;": '\U00002A75', |
"EqualTilde;": '\U00002242', |
"Equilibrium;": '\U000021CC', |
"Escr;": '\U00002130', |
"Esim;": '\U00002A73', |
"Eta;": '\U00000397', |
"Euml;": '\U000000CB', |
"Exists;": '\U00002203', |
"ExponentialE;": '\U00002147', |
"Fcy;": '\U00000424', |
"Ffr;": '\U0001D509', |
"FilledSmallSquare;": '\U000025FC', |
"FilledVerySmallSquare;": '\U000025AA', |
"Fopf;": '\U0001D53D', |
"ForAll;": '\U00002200', |
"Fouriertrf;": '\U00002131', |
"Fscr;": '\U00002131', |
"GJcy;": '\U00000403', |
"GT;": '\U0000003E', |
"Gamma;": '\U00000393', |
"Gammad;": '\U000003DC', |
"Gbreve;": '\U0000011E', |
"Gcedil;": '\U00000122', |
"Gcirc;": '\U0000011C', |
"Gcy;": '\U00000413', |
"Gdot;": '\U00000120', |
"Gfr;": '\U0001D50A', |
"Gg;": '\U000022D9', |
"Gopf;": '\U0001D53E', |
"GreaterEqual;": '\U00002265', |
"GreaterEqualLess;": '\U000022DB', |
"GreaterFullEqual;": '\U00002267', |
"GreaterGreater;": '\U00002AA2', |
"GreaterLess;": '\U00002277', |
"GreaterSlantEqual;": '\U00002A7E', |
"GreaterTilde;": '\U00002273', |
"Gscr;": '\U0001D4A2', |
"Gt;": '\U0000226B', |
"HARDcy;": '\U0000042A', |
"Hacek;": '\U000002C7', |
"Hat;": '\U0000005E', |
"Hcirc;": '\U00000124', |
"Hfr;": '\U0000210C', |
"HilbertSpace;": '\U0000210B', |
"Hopf;": '\U0000210D', |
"HorizontalLine;": '\U00002500', |
"Hscr;": '\U0000210B', |
"Hstrok;": '\U00000126', |
"HumpDownHump;": '\U0000224E', |
"HumpEqual;": '\U0000224F', |
"IEcy;": '\U00000415', |
"IJlig;": '\U00000132', |
"IOcy;": '\U00000401', |
"Iacute;": '\U000000CD', |
"Icirc;": '\U000000CE', |
"Icy;": '\U00000418', |
"Idot;": '\U00000130', |
"Ifr;": '\U00002111', |
"Igrave;": '\U000000CC', |
"Im;": '\U00002111', |
"Imacr;": '\U0000012A', |
"ImaginaryI;": '\U00002148', |
"Implies;": '\U000021D2', |
"Int;": '\U0000222C', |
"Integral;": '\U0000222B', |
"Intersection;": '\U000022C2', |
"InvisibleComma;": '\U00002063', |
"InvisibleTimes;": '\U00002062', |
"Iogon;": '\U0000012E', |
"Iopf;": '\U0001D540', |
"Iota;": '\U00000399', |
"Iscr;": '\U00002110', |
"Itilde;": '\U00000128', |
"Iukcy;": '\U00000406', |
"Iuml;": '\U000000CF', |
"Jcirc;": '\U00000134', |
"Jcy;": '\U00000419', |
"Jfr;": '\U0001D50D', |
"Jopf;": '\U0001D541', |
"Jscr;": '\U0001D4A5', |
"Jsercy;": '\U00000408', |
"Jukcy;": '\U00000404', |
"KHcy;": '\U00000425', |
"KJcy;": '\U0000040C', |
"Kappa;": '\U0000039A', |
"Kcedil;": '\U00000136', |
"Kcy;": '\U0000041A', |
"Kfr;": '\U0001D50E', |
"Kopf;": '\U0001D542', |
"Kscr;": '\U0001D4A6', |
"LJcy;": '\U00000409', |
"LT;": '\U0000003C', |
"Lacute;": '\U00000139', |
"Lambda;": '\U0000039B', |
"Lang;": '\U000027EA', |
"Laplacetrf;": '\U00002112', |
"Larr;": '\U0000219E', |
"Lcaron;": '\U0000013D', |
"Lcedil;": '\U0000013B', |
"Lcy;": '\U0000041B', |
"LeftAngleBracket;": '\U000027E8', |
"LeftArrow;": '\U00002190', |
"LeftArrowBar;": '\U000021E4', |
"LeftArrowRightArrow;": '\U000021C6', |
"LeftCeiling;": '\U00002308', |
"LeftDoubleBracket;": '\U000027E6', |
"LeftDownTeeVector;": '\U00002961', |
"LeftDownVector;": '\U000021C3', |
"LeftDownVectorBar;": '\U00002959', |
"LeftFloor;": '\U0000230A', |
"LeftRightArrow;": '\U00002194', |
"LeftRightVector;": '\U0000294E', |
"LeftTee;": '\U000022A3', |
"LeftTeeArrow;": '\U000021A4', |
"LeftTeeVector;": '\U0000295A', |
"LeftTriangle;": '\U000022B2', |
"LeftTriangleBar;": '\U000029CF', |
"LeftTriangleEqual;": '\U000022B4', |
"LeftUpDownVector;": '\U00002951', |
"LeftUpTeeVector;": '\U00002960', |
"LeftUpVector;": '\U000021BF', |
"LeftUpVectorBar;": '\U00002958', |
"LeftVector;": '\U000021BC', |
"LeftVectorBar;": '\U00002952', |
"Leftarrow;": '\U000021D0', |
"Leftrightarrow;": '\U000021D4', |
"LessEqualGreater;": '\U000022DA', |
"LessFullEqual;": '\U00002266', |
"LessGreater;": '\U00002276', |
"LessLess;": '\U00002AA1', |
"LessSlantEqual;": '\U00002A7D', |
"LessTilde;": '\U00002272', |
"Lfr;": '\U0001D50F', |
"Ll;": '\U000022D8', |
"Lleftarrow;": '\U000021DA', |
"Lmidot;": '\U0000013F', |
"LongLeftArrow;": '\U000027F5', |
"LongLeftRightArrow;": '\U000027F7', |
"LongRightArrow;": '\U000027F6', |
"Longleftarrow;": '\U000027F8', |
"Longleftrightarrow;": '\U000027FA', |
"Longrightarrow;": '\U000027F9', |
"Lopf;": '\U0001D543', |
"LowerLeftArrow;": '\U00002199', |
"LowerRightArrow;": '\U00002198', |
"Lscr;": '\U00002112', |
"Lsh;": '\U000021B0', |
"Lstrok;": '\U00000141', |
"Lt;": '\U0000226A', |
"Map;": '\U00002905', |
"Mcy;": '\U0000041C', |
"MediumSpace;": '\U0000205F', |
"Mellintrf;": '\U00002133', |
"Mfr;": '\U0001D510', |
"MinusPlus;": '\U00002213', |
"Mopf;": '\U0001D544', |
"Mscr;": '\U00002133', |
"Mu;": '\U0000039C', |
"NJcy;": '\U0000040A', |
"Nacute;": '\U00000143', |
"Ncaron;": '\U00000147', |
"Ncedil;": '\U00000145', |
"Ncy;": '\U0000041D', |
"NegativeMediumSpace;": '\U0000200B', |
"NegativeThickSpace;": '\U0000200B', |
"NegativeThinSpace;": '\U0000200B', |
"NegativeVeryThinSpace;": '\U0000200B', |
"NestedGreaterGreater;": '\U0000226B', |
"NestedLessLess;": '\U0000226A', |
"NewLine;": '\U0000000A', |
"Nfr;": '\U0001D511', |
"NoBreak;": '\U00002060', |
"NonBreakingSpace;": '\U000000A0', |
"Nopf;": '\U00002115', |
"Not;": '\U00002AEC', |
"NotCongruent;": '\U00002262', |
"NotCupCap;": '\U0000226D', |
"NotDoubleVerticalBar;": '\U00002226', |
"NotElement;": '\U00002209', |
"NotEqual;": '\U00002260', |
"NotExists;": '\U00002204', |
"NotGreater;": '\U0000226F', |
"NotGreaterEqual;": '\U00002271', |
"NotGreaterLess;": '\U00002279', |
"NotGreaterTilde;": '\U00002275', |
"NotLeftTriangle;": '\U000022EA', |
"NotLeftTriangleEqual;": '\U000022EC', |
"NotLess;": '\U0000226E', |
"NotLessEqual;": '\U00002270', |
"NotLessGreater;": '\U00002278', |
"NotLessTilde;": '\U00002274', |
"NotPrecedes;": '\U00002280', |
"NotPrecedesSlantEqual;": '\U000022E0', |
"NotReverseElement;": '\U0000220C', |
"NotRightTriangle;": '\U000022EB', |
"NotRightTriangleEqual;": '\U000022ED', |
"NotSquareSubsetEqual;": '\U000022E2', |
"NotSquareSupersetEqual;": '\U000022E3', |
"NotSubsetEqual;": '\U00002288', |
"NotSucceeds;": '\U00002281', |
"NotSucceedsSlantEqual;": '\U000022E1', |
"NotSupersetEqual;": '\U00002289', |
"NotTilde;": '\U00002241', |
"NotTildeEqual;": '\U00002244', |
"NotTildeFullEqual;": '\U00002247', |
"NotTildeTilde;": '\U00002249', |
"NotVerticalBar;": '\U00002224', |
"Nscr;": '\U0001D4A9', |
"Ntilde;": '\U000000D1', |
"Nu;": '\U0000039D', |
"OElig;": '\U00000152', |
"Oacute;": '\U000000D3', |
"Ocirc;": '\U000000D4', |
"Ocy;": '\U0000041E', |
"Odblac;": '\U00000150', |
"Ofr;": '\U0001D512', |
"Ograve;": '\U000000D2', |
"Omacr;": '\U0000014C', |
"Omega;": '\U000003A9', |
"Omicron;": '\U0000039F', |
"Oopf;": '\U0001D546', |
"OpenCurlyDoubleQuote;": '\U0000201C', |
"OpenCurlyQuote;": '\U00002018', |
"Or;": '\U00002A54', |
"Oscr;": '\U0001D4AA', |
"Oslash;": '\U000000D8', |
"Otilde;": '\U000000D5', |
"Otimes;": '\U00002A37', |
"Ouml;": '\U000000D6', |
"OverBar;": '\U0000203E', |
"OverBrace;": '\U000023DE', |
"OverBracket;": '\U000023B4', |
"OverParenthesis;": '\U000023DC', |
"PartialD;": '\U00002202', |
"Pcy;": '\U0000041F', |
"Pfr;": '\U0001D513', |
"Phi;": '\U000003A6', |
"Pi;": '\U000003A0', |
"PlusMinus;": '\U000000B1', |
"Poincareplane;": '\U0000210C', |
"Popf;": '\U00002119', |
"Pr;": '\U00002ABB', |
"Precedes;": '\U0000227A', |
"PrecedesEqual;": '\U00002AAF', |
"PrecedesSlantEqual;": '\U0000227C', |
"PrecedesTilde;": '\U0000227E', |
"Prime;": '\U00002033', |
"Product;": '\U0000220F', |
"Proportion;": '\U00002237', |
"Proportional;": '\U0000221D', |
"Pscr;": '\U0001D4AB', |
"Psi;": '\U000003A8', |
"QUOT;": '\U00000022', |
"Qfr;": '\U0001D514', |
"Qopf;": '\U0000211A', |
"Qscr;": '\U0001D4AC', |
"RBarr;": '\U00002910', |
"REG;": '\U000000AE', |
"Racute;": '\U00000154', |
"Rang;": '\U000027EB', |
"Rarr;": '\U000021A0', |
"Rarrtl;": '\U00002916', |
"Rcaron;": '\U00000158', |
"Rcedil;": '\U00000156', |
"Rcy;": '\U00000420', |
"Re;": '\U0000211C', |
"ReverseElement;": '\U0000220B', |
"ReverseEquilibrium;": '\U000021CB', |
"ReverseUpEquilibrium;": '\U0000296F', |
"Rfr;": '\U0000211C', |
"Rho;": '\U000003A1', |
"RightAngleBracket;": '\U000027E9', |
"RightArrow;": '\U00002192', |
"RightArrowBar;": '\U000021E5', |
"RightArrowLeftArrow;": '\U000021C4', |
"RightCeiling;": '\U00002309', |
"RightDoubleBracket;": '\U000027E7', |
"RightDownTeeVector;": '\U0000295D', |
"RightDownVector;": '\U000021C2', |
"RightDownVectorBar;": '\U00002955', |
"RightFloor;": '\U0000230B', |
"RightTee;": '\U000022A2', |
"RightTeeArrow;": '\U000021A6', |
"RightTeeVector;": '\U0000295B', |
"RightTriangle;": '\U000022B3', |
"RightTriangleBar;": '\U000029D0', |
"RightTriangleEqual;": '\U000022B5', |
"RightUpDownVector;": '\U0000294F', |
"RightUpTeeVector;": '\U0000295C', |
"RightUpVector;": '\U000021BE', |
"RightUpVectorBar;": '\U00002954', |
"RightVector;": '\U000021C0', |
"RightVectorBar;": '\U00002953', |
"Rightarrow;": '\U000021D2', |
"Ropf;": '\U0000211D', |
"RoundImplies;": '\U00002970', |
"Rrightarrow;": '\U000021DB', |
"Rscr;": '\U0000211B', |
"Rsh;": '\U000021B1', |
"RuleDelayed;": '\U000029F4', |
"SHCHcy;": '\U00000429', |
"SHcy;": '\U00000428', |
"SOFTcy;": '\U0000042C', |
"Sacute;": '\U0000015A', |
"Sc;": '\U00002ABC', |
"Scaron;": '\U00000160', |
"Scedil;": '\U0000015E', |
"Scirc;": '\U0000015C', |
"Scy;": '\U00000421', |
"Sfr;": '\U0001D516', |
"ShortDownArrow;": '\U00002193', |
"ShortLeftArrow;": '\U00002190', |
"ShortRightArrow;": '\U00002192', |
"ShortUpArrow;": '\U00002191', |
"Sigma;": '\U000003A3', |
"SmallCircle;": '\U00002218', |
"Sopf;": '\U0001D54A', |
"Sqrt;": '\U0000221A', |
"Square;": '\U000025A1', |
"SquareIntersection;": '\U00002293', |
"SquareSubset;": '\U0000228F', |
"SquareSubsetEqual;": '\U00002291', |
"SquareSuperset;": '\U00002290', |
"SquareSupersetEqual;": '\U00002292', |
"SquareUnion;": '\U00002294', |
"Sscr;": '\U0001D4AE', |
"Star;": '\U000022C6', |
"Sub;": '\U000022D0', |
"Subset;": '\U000022D0', |
"SubsetEqual;": '\U00002286', |
"Succeeds;": '\U0000227B', |
"SucceedsEqual;": '\U00002AB0', |
"SucceedsSlantEqual;": '\U0000227D', |
"SucceedsTilde;": '\U0000227F', |
"SuchThat;": '\U0000220B', |
"Sum;": '\U00002211', |
"Sup;": '\U000022D1', |
"Superset;": '\U00002283', |
"SupersetEqual;": '\U00002287', |
"Supset;": '\U000022D1', |
"THORN;": '\U000000DE', |
"TRADE;": '\U00002122', |
"TSHcy;": '\U0000040B', |
"TScy;": '\U00000426', |
"Tab;": '\U00000009', |
"Tau;": '\U000003A4', |
"Tcaron;": '\U00000164', |
"Tcedil;": '\U00000162', |
"Tcy;": '\U00000422', |
"Tfr;": '\U0001D517', |
"Therefore;": '\U00002234', |
"Theta;": '\U00000398', |
"ThinSpace;": '\U00002009', |
"Tilde;": '\U0000223C', |
"TildeEqual;": '\U00002243', |
"TildeFullEqual;": '\U00002245', |
"TildeTilde;": '\U00002248', |
"Topf;": '\U0001D54B', |
"TripleDot;": '\U000020DB', |
"Tscr;": '\U0001D4AF', |
"Tstrok;": '\U00000166', |
"Uacute;": '\U000000DA', |
"Uarr;": '\U0000219F', |
"Uarrocir;": '\U00002949', |
"Ubrcy;": '\U0000040E', |
"Ubreve;": '\U0000016C', |
"Ucirc;": '\U000000DB', |
"Ucy;": '\U00000423', |
"Udblac;": '\U00000170', |
"Ufr;": '\U0001D518', |
"Ugrave;": '\U000000D9', |
"Umacr;": '\U0000016A', |
"UnderBar;": '\U0000005F', |
"UnderBrace;": '\U000023DF', |
"UnderBracket;": '\U000023B5', |
"UnderParenthesis;": '\U000023DD', |
"Union;": '\U000022C3', |
"UnionPlus;": '\U0000228E', |
"Uogon;": '\U00000172', |
"Uopf;": '\U0001D54C', |
"UpArrow;": '\U00002191', |
"UpArrowBar;": '\U00002912', |
"UpArrowDownArrow;": '\U000021C5', |
"UpDownArrow;": '\U00002195', |
"UpEquilibrium;": '\U0000296E', |
"UpTee;": '\U000022A5', |
"UpTeeArrow;": '\U000021A5', |
"Uparrow;": '\U000021D1', |
"Updownarrow;": '\U000021D5', |
"UpperLeftArrow;": '\U00002196', |
"UpperRightArrow;": '\U00002197', |
"Upsi;": '\U000003D2', |
"Upsilon;": '\U000003A5', |
"Uring;": '\U0000016E', |
"Uscr;": '\U0001D4B0', |
"Utilde;": '\U00000168', |
"Uuml;": '\U000000DC', |
"VDash;": '\U000022AB', |
"Vbar;": '\U00002AEB', |
"Vcy;": '\U00000412', |
"Vdash;": '\U000022A9', |
"Vdashl;": '\U00002AE6', |
"Vee;": '\U000022C1', |
"Verbar;": '\U00002016', |
"Vert;": '\U00002016', |
"VerticalBar;": '\U00002223', |
"VerticalLine;": '\U0000007C', |
"VerticalSeparator;": '\U00002758', |
"VerticalTilde;": '\U00002240', |
"VeryThinSpace;": '\U0000200A', |
"Vfr;": '\U0001D519', |
"Vopf;": '\U0001D54D', |
"Vscr;": '\U0001D4B1', |
"Vvdash;": '\U000022AA', |
"Wcirc;": '\U00000174', |
"Wedge;": '\U000022C0', |
"Wfr;": '\U0001D51A', |
"Wopf;": '\U0001D54E', |
"Wscr;": '\U0001D4B2', |
"Xfr;": '\U0001D51B', |
"Xi;": '\U0000039E', |
"Xopf;": '\U0001D54F', |
"Xscr;": '\U0001D4B3', |
"YAcy;": '\U0000042F', |
"YIcy;": '\U00000407', |
"YUcy;": '\U0000042E', |
"Yacute;": '\U000000DD', |
"Ycirc;": '\U00000176', |
"Ycy;": '\U0000042B', |
"Yfr;": '\U0001D51C', |
"Yopf;": '\U0001D550', |
"Yscr;": '\U0001D4B4', |
"Yuml;": '\U00000178', |
"ZHcy;": '\U00000416', |
"Zacute;": '\U00000179', |
"Zcaron;": '\U0000017D', |
"Zcy;": '\U00000417', |
"Zdot;": '\U0000017B', |
"ZeroWidthSpace;": '\U0000200B', |
"Zeta;": '\U00000396', |
"Zfr;": '\U00002128', |
"Zopf;": '\U00002124', |
"Zscr;": '\U0001D4B5', |
"aacute;": '\U000000E1', |
"abreve;": '\U00000103', |
"ac;": '\U0000223E', |
"acd;": '\U0000223F', |
"acirc;": '\U000000E2', |
"acute;": '\U000000B4', |
"acy;": '\U00000430', |
"aelig;": '\U000000E6', |
"af;": '\U00002061', |
"afr;": '\U0001D51E', |
"agrave;": '\U000000E0', |
"alefsym;": '\U00002135', |
"aleph;": '\U00002135', |
"alpha;": '\U000003B1', |
"amacr;": '\U00000101', |
"amalg;": '\U00002A3F', |
"amp;": '\U00000026', |
"and;": '\U00002227', |
"andand;": '\U00002A55', |
"andd;": '\U00002A5C', |
"andslope;": '\U00002A58', |
"andv;": '\U00002A5A', |
"ang;": '\U00002220', |
"ange;": '\U000029A4', |
"angle;": '\U00002220', |
"angmsd;": '\U00002221', |
"angmsdaa;": '\U000029A8', |
"angmsdab;": '\U000029A9', |
"angmsdac;": '\U000029AA', |
"angmsdad;": '\U000029AB', |
"angmsdae;": '\U000029AC', |
"angmsdaf;": '\U000029AD', |
"angmsdag;": '\U000029AE', |
"angmsdah;": '\U000029AF', |
"angrt;": '\U0000221F', |
"angrtvb;": '\U000022BE', |
"angrtvbd;": '\U0000299D', |
"angsph;": '\U00002222', |
"angst;": '\U000000C5', |
"angzarr;": '\U0000237C', |
"aogon;": '\U00000105', |
"aopf;": '\U0001D552', |
"ap;": '\U00002248', |
"apE;": '\U00002A70', |
"apacir;": '\U00002A6F', |
"ape;": '\U0000224A', |
"apid;": '\U0000224B', |
"apos;": '\U00000027', |
"approx;": '\U00002248', |
"approxeq;": '\U0000224A', |
"aring;": '\U000000E5', |
"ascr;": '\U0001D4B6', |
"ast;": '\U0000002A', |
"asymp;": '\U00002248', |
"asympeq;": '\U0000224D', |
"atilde;": '\U000000E3', |
"auml;": '\U000000E4', |
"awconint;": '\U00002233', |
"awint;": '\U00002A11', |
"bNot;": '\U00002AED', |
"backcong;": '\U0000224C', |
"backepsilon;": '\U000003F6', |
"backprime;": '\U00002035', |
"backsim;": '\U0000223D', |
"backsimeq;": '\U000022CD', |
"barvee;": '\U000022BD', |
"barwed;": '\U00002305', |
"barwedge;": '\U00002305', |
"bbrk;": '\U000023B5', |
"bbrktbrk;": '\U000023B6', |
"bcong;": '\U0000224C', |
"bcy;": '\U00000431', |
"bdquo;": '\U0000201E', |
"becaus;": '\U00002235', |
"because;": '\U00002235', |
"bemptyv;": '\U000029B0', |
"bepsi;": '\U000003F6', |
"bernou;": '\U0000212C', |
"beta;": '\U000003B2', |
"beth;": '\U00002136', |
"between;": '\U0000226C', |
"bfr;": '\U0001D51F', |
"bigcap;": '\U000022C2', |
"bigcirc;": '\U000025EF', |
"bigcup;": '\U000022C3', |
"bigodot;": '\U00002A00', |
"bigoplus;": '\U00002A01', |
"bigotimes;": '\U00002A02', |
"bigsqcup;": '\U00002A06', |
"bigstar;": '\U00002605', |
"bigtriangledown;": '\U000025BD', |
"bigtriangleup;": '\U000025B3', |
"biguplus;": '\U00002A04', |
"bigvee;": '\U000022C1', |
"bigwedge;": '\U000022C0', |
"bkarow;": '\U0000290D', |
"blacklozenge;": '\U000029EB', |
"blacksquare;": '\U000025AA', |
"blacktriangle;": '\U000025B4', |
"blacktriangledown;": '\U000025BE', |
"blacktriangleleft;": '\U000025C2', |
"blacktriangleright;": '\U000025B8', |
"blank;": '\U00002423', |
"blk12;": '\U00002592', |
"blk14;": '\U00002591', |
"blk34;": '\U00002593', |
"block;": '\U00002588', |
"bnot;": '\U00002310', |
"bopf;": '\U0001D553', |
"bot;": '\U000022A5', |
"bottom;": '\U000022A5', |
"bowtie;": '\U000022C8', |
"boxDL;": '\U00002557', |
"boxDR;": '\U00002554', |
"boxDl;": '\U00002556', |
"boxDr;": '\U00002553', |
"boxH;": '\U00002550', |
"boxHD;": '\U00002566', |
"boxHU;": '\U00002569', |
"boxHd;": '\U00002564', |
"boxHu;": '\U00002567', |
"boxUL;": '\U0000255D', |
"boxUR;": '\U0000255A', |
"boxUl;": '\U0000255C', |
"boxUr;": '\U00002559', |
"boxV;": '\U00002551', |
"boxVH;": '\U0000256C', |
"boxVL;": '\U00002563', |
"boxVR;": '\U00002560', |
"boxVh;": '\U0000256B', |
"boxVl;": '\U00002562', |
"boxVr;": '\U0000255F', |
"boxbox;": '\U000029C9', |
"boxdL;": '\U00002555', |
"boxdR;": '\U00002552', |
"boxdl;": '\U00002510', |
"boxdr;": '\U0000250C', |
"boxh;": '\U00002500', |
"boxhD;": '\U00002565', |
"boxhU;": '\U00002568', |
"boxhd;": '\U0000252C', |
"boxhu;": '\U00002534', |
"boxminus;": '\U0000229F', |
"boxplus;": '\U0000229E', |
"boxtimes;": '\U000022A0', |
"boxuL;": '\U0000255B', |
"boxuR;": '\U00002558', |
"boxul;": '\U00002518', |
"boxur;": '\U00002514', |
"boxv;": '\U00002502', |
"boxvH;": '\U0000256A', |
"boxvL;": '\U00002561', |
"boxvR;": '\U0000255E', |
"boxvh;": '\U0000253C', |
"boxvl;": '\U00002524', |
"boxvr;": '\U0000251C', |
"bprime;": '\U00002035', |
"breve;": '\U000002D8', |
"brvbar;": '\U000000A6', |
"bscr;": '\U0001D4B7', |
"bsemi;": '\U0000204F', |
"bsim;": '\U0000223D', |
"bsime;": '\U000022CD', |
"bsol;": '\U0000005C', |
"bsolb;": '\U000029C5', |
"bsolhsub;": '\U000027C8', |
"bull;": '\U00002022', |
"bullet;": '\U00002022', |
"bump;": '\U0000224E', |
"bumpE;": '\U00002AAE', |
"bumpe;": '\U0000224F', |
"bumpeq;": '\U0000224F', |
"cacute;": '\U00000107', |
"cap;": '\U00002229', |
"capand;": '\U00002A44', |
"capbrcup;": '\U00002A49', |
"capcap;": '\U00002A4B', |
"capcup;": '\U00002A47', |
"capdot;": '\U00002A40', |
"caret;": '\U00002041', |
"caron;": '\U000002C7', |
"ccaps;": '\U00002A4D', |
"ccaron;": '\U0000010D', |
"ccedil;": '\U000000E7', |
"ccirc;": '\U00000109', |
"ccups;": '\U00002A4C', |
"ccupssm;": '\U00002A50', |
"cdot;": '\U0000010B', |
"cedil;": '\U000000B8', |
"cemptyv;": '\U000029B2', |
"cent;": '\U000000A2', |
"centerdot;": '\U000000B7', |
"cfr;": '\U0001D520', |
"chcy;": '\U00000447', |
"check;": '\U00002713', |
"checkmark;": '\U00002713', |
"chi;": '\U000003C7', |
"cir;": '\U000025CB', |
"cirE;": '\U000029C3', |
"circ;": '\U000002C6', |
"circeq;": '\U00002257', |
"circlearrowleft;": '\U000021BA', |
"circlearrowright;": '\U000021BB', |
"circledR;": '\U000000AE', |
"circledS;": '\U000024C8', |
"circledast;": '\U0000229B', |
"circledcirc;": '\U0000229A', |
"circleddash;": '\U0000229D', |
"cire;": '\U00002257', |
"cirfnint;": '\U00002A10', |
"cirmid;": '\U00002AEF', |
"cirscir;": '\U000029C2', |
"clubs;": '\U00002663', |
"clubsuit;": '\U00002663', |
"colon;": '\U0000003A', |
"colone;": '\U00002254', |
"coloneq;": '\U00002254', |
"comma;": '\U0000002C', |
"commat;": '\U00000040', |
"comp;": '\U00002201', |
"compfn;": '\U00002218', |
"complement;": '\U00002201', |
"complexes;": '\U00002102', |
"cong;": '\U00002245', |
"congdot;": '\U00002A6D', |
"conint;": '\U0000222E', |
"copf;": '\U0001D554', |
"coprod;": '\U00002210', |
"copy;": '\U000000A9', |
"copysr;": '\U00002117', |
"crarr;": '\U000021B5', |
"cross;": '\U00002717', |
"cscr;": '\U0001D4B8', |
"csub;": '\U00002ACF', |
"csube;": '\U00002AD1', |
"csup;": '\U00002AD0', |
"csupe;": '\U00002AD2', |
"ctdot;": '\U000022EF', |
"cudarrl;": '\U00002938', |
"cudarrr;": '\U00002935', |
"cuepr;": '\U000022DE', |
"cuesc;": '\U000022DF', |
"cularr;": '\U000021B6', |
"cularrp;": '\U0000293D', |
"cup;": '\U0000222A', |
"cupbrcap;": '\U00002A48', |
"cupcap;": '\U00002A46', |
"cupcup;": '\U00002A4A', |
"cupdot;": '\U0000228D', |
"cupor;": '\U00002A45', |
"curarr;": '\U000021B7', |
"curarrm;": '\U0000293C', |
"curlyeqprec;": '\U000022DE', |
"curlyeqsucc;": '\U000022DF', |
"curlyvee;": '\U000022CE', |
"curlywedge;": '\U000022CF', |
"curren;": '\U000000A4', |
"curvearrowleft;": '\U000021B6', |
"curvearrowright;": '\U000021B7', |
"cuvee;": '\U000022CE', |
"cuwed;": '\U000022CF', |
"cwconint;": '\U00002232', |
"cwint;": '\U00002231', |
"cylcty;": '\U0000232D', |
"dArr;": '\U000021D3', |
"dHar;": '\U00002965', |
"dagger;": '\U00002020', |
"daleth;": '\U00002138', |
"darr;": '\U00002193', |
"dash;": '\U00002010', |
"dashv;": '\U000022A3', |
"dbkarow;": '\U0000290F', |
"dblac;": '\U000002DD', |
"dcaron;": '\U0000010F', |
"dcy;": '\U00000434', |
"dd;": '\U00002146', |
"ddagger;": '\U00002021', |
"ddarr;": '\U000021CA', |
"ddotseq;": '\U00002A77', |
"deg;": '\U000000B0', |
"delta;": '\U000003B4', |
"demptyv;": '\U000029B1', |
"dfisht;": '\U0000297F', |
"dfr;": '\U0001D521', |
"dharl;": '\U000021C3', |
"dharr;": '\U000021C2', |
"diam;": '\U000022C4', |
"diamond;": '\U000022C4', |
"diamondsuit;": '\U00002666', |
"diams;": '\U00002666', |
"die;": '\U000000A8', |
"digamma;": '\U000003DD', |
"disin;": '\U000022F2', |
"div;": '\U000000F7', |
"divide;": '\U000000F7', |
"divideontimes;": '\U000022C7', |
"divonx;": '\U000022C7', |
"djcy;": '\U00000452', |
"dlcorn;": '\U0000231E', |
"dlcrop;": '\U0000230D', |
"dollar;": '\U00000024', |
"dopf;": '\U0001D555', |
"dot;": '\U000002D9', |
"doteq;": '\U00002250', |
"doteqdot;": '\U00002251', |
"dotminus;": '\U00002238', |
"dotplus;": '\U00002214', |
"dotsquare;": '\U000022A1', |
"doublebarwedge;": '\U00002306', |
"downarrow;": '\U00002193', |
"downdownarrows;": '\U000021CA', |
"downharpoonleft;": '\U000021C3', |
"downharpoonright;": '\U000021C2', |
"drbkarow;": '\U00002910', |
"drcorn;": '\U0000231F', |
"drcrop;": '\U0000230C', |
"dscr;": '\U0001D4B9', |
"dscy;": '\U00000455', |
"dsol;": '\U000029F6', |
"dstrok;": '\U00000111', |
"dtdot;": '\U000022F1', |
"dtri;": '\U000025BF', |
"dtrif;": '\U000025BE', |
"duarr;": '\U000021F5', |
"duhar;": '\U0000296F', |
"dwangle;": '\U000029A6', |
"dzcy;": '\U0000045F', |
"dzigrarr;": '\U000027FF', |
"eDDot;": '\U00002A77', |
"eDot;": '\U00002251', |
"eacute;": '\U000000E9', |
"easter;": '\U00002A6E', |
"ecaron;": '\U0000011B', |
"ecir;": '\U00002256', |
"ecirc;": '\U000000EA', |
"ecolon;": '\U00002255', |
"ecy;": '\U0000044D', |
"edot;": '\U00000117', |
"ee;": '\U00002147', |
"efDot;": '\U00002252', |
"efr;": '\U0001D522', |
"eg;": '\U00002A9A', |
"egrave;": '\U000000E8', |
"egs;": '\U00002A96', |
"egsdot;": '\U00002A98', |
"el;": '\U00002A99', |
"elinters;": '\U000023E7', |
"ell;": '\U00002113', |
"els;": '\U00002A95', |
"elsdot;": '\U00002A97', |
"emacr;": '\U00000113', |
"empty;": '\U00002205', |
"emptyset;": '\U00002205', |
"emptyv;": '\U00002205', |
"emsp;": '\U00002003', |
"emsp13;": '\U00002004', |
"emsp14;": '\U00002005', |
"eng;": '\U0000014B', |
"ensp;": '\U00002002', |
"eogon;": '\U00000119', |
"eopf;": '\U0001D556', |
"epar;": '\U000022D5', |
"eparsl;": '\U000029E3', |
"eplus;": '\U00002A71', |
"epsi;": '\U000003B5', |
"epsilon;": '\U000003B5', |
"epsiv;": '\U000003F5', |
"eqcirc;": '\U00002256', |
"eqcolon;": '\U00002255', |
"eqsim;": '\U00002242', |
"eqslantgtr;": '\U00002A96', |
"eqslantless;": '\U00002A95', |
"equals;": '\U0000003D', |
"equest;": '\U0000225F', |
"equiv;": '\U00002261', |
"equivDD;": '\U00002A78', |
"eqvparsl;": '\U000029E5', |
"erDot;": '\U00002253', |
"erarr;": '\U00002971', |
"escr;": '\U0000212F', |
"esdot;": '\U00002250', |
"esim;": '\U00002242', |
"eta;": '\U000003B7', |
"eth;": '\U000000F0', |
"euml;": '\U000000EB', |
"euro;": '\U000020AC', |
"excl;": '\U00000021', |
"exist;": '\U00002203', |
"expectation;": '\U00002130', |
"exponentiale;": '\U00002147', |
"fallingdotseq;": '\U00002252', |
"fcy;": '\U00000444', |
"female;": '\U00002640', |
"ffilig;": '\U0000FB03', |
"fflig;": '\U0000FB00', |
"ffllig;": '\U0000FB04', |
"ffr;": '\U0001D523', |
"filig;": '\U0000FB01', |
"flat;": '\U0000266D', |
"fllig;": '\U0000FB02', |
"fltns;": '\U000025B1', |
"fnof;": '\U00000192', |
"fopf;": '\U0001D557', |
"forall;": '\U00002200', |
"fork;": '\U000022D4', |
"forkv;": '\U00002AD9', |
"fpartint;": '\U00002A0D', |
"frac12;": '\U000000BD', |
"frac13;": '\U00002153', |
"frac14;": '\U000000BC', |
"frac15;": '\U00002155', |
"frac16;": '\U00002159', |
"frac18;": '\U0000215B', |
"frac23;": '\U00002154', |
"frac25;": '\U00002156', |
"frac34;": '\U000000BE', |
"frac35;": '\U00002157', |
"frac38;": '\U0000215C', |
"frac45;": '\U00002158', |
"frac56;": '\U0000215A', |
"frac58;": '\U0000215D', |
"frac78;": '\U0000215E', |
"frasl;": '\U00002044', |
"frown;": '\U00002322', |
"fscr;": '\U0001D4BB', |
"gE;": '\U00002267', |
"gEl;": '\U00002A8C', |
"gacute;": '\U000001F5', |
"gamma;": '\U000003B3', |
"gammad;": '\U000003DD', |
"gap;": '\U00002A86', |
"gbreve;": '\U0000011F', |
"gcirc;": '\U0000011D', |
"gcy;": '\U00000433', |
"gdot;": '\U00000121', |
"ge;": '\U00002265', |
"gel;": '\U000022DB', |
"geq;": '\U00002265', |
"geqq;": '\U00002267', |
"geqslant;": '\U00002A7E', |
"ges;": '\U00002A7E', |
"gescc;": '\U00002AA9', |
"gesdot;": '\U00002A80', |
"gesdoto;": '\U00002A82', |
"gesdotol;": '\U00002A84', |
"gesles;": '\U00002A94', |
"gfr;": '\U0001D524', |
"gg;": '\U0000226B', |
"ggg;": '\U000022D9', |
"gimel;": '\U00002137', |
"gjcy;": '\U00000453', |
"gl;": '\U00002277', |
"glE;": '\U00002A92', |
"gla;": '\U00002AA5', |
"glj;": '\U00002AA4', |
"gnE;": '\U00002269', |
"gnap;": '\U00002A8A', |
"gnapprox;": '\U00002A8A', |
"gne;": '\U00002A88', |
"gneq;": '\U00002A88', |
"gneqq;": '\U00002269', |
"gnsim;": '\U000022E7', |
"gopf;": '\U0001D558', |
"grave;": '\U00000060', |
"gscr;": '\U0000210A', |
"gsim;": '\U00002273', |
"gsime;": '\U00002A8E', |
"gsiml;": '\U00002A90', |
"gt;": '\U0000003E', |
"gtcc;": '\U00002AA7', |
"gtcir;": '\U00002A7A', |
"gtdot;": '\U000022D7', |
"gtlPar;": '\U00002995', |
"gtquest;": '\U00002A7C', |
"gtrapprox;": '\U00002A86', |
"gtrarr;": '\U00002978', |
"gtrdot;": '\U000022D7', |
"gtreqless;": '\U000022DB', |
"gtreqqless;": '\U00002A8C', |
"gtrless;": '\U00002277', |
"gtrsim;": '\U00002273', |
"hArr;": '\U000021D4', |
"hairsp;": '\U0000200A', |
"half;": '\U000000BD', |
"hamilt;": '\U0000210B', |
"hardcy;": '\U0000044A', |
"harr;": '\U00002194', |
"harrcir;": '\U00002948', |
"harrw;": '\U000021AD', |
"hbar;": '\U0000210F', |
"hcirc;": '\U00000125', |
"hearts;": '\U00002665', |
"heartsuit;": '\U00002665', |
"hellip;": '\U00002026', |
"hercon;": '\U000022B9', |
"hfr;": '\U0001D525', |
"hksearow;": '\U00002925', |
"hkswarow;": '\U00002926', |
"hoarr;": '\U000021FF', |
"homtht;": '\U0000223B', |
"hookleftarrow;": '\U000021A9', |
"hookrightarrow;": '\U000021AA', |
"hopf;": '\U0001D559', |
"horbar;": '\U00002015', |
"hscr;": '\U0001D4BD', |
"hslash;": '\U0000210F', |
"hstrok;": '\U00000127', |
"hybull;": '\U00002043', |
"hyphen;": '\U00002010', |
"iacute;": '\U000000ED', |
"ic;": '\U00002063', |
"icirc;": '\U000000EE', |
"icy;": '\U00000438', |
"iecy;": '\U00000435', |
"iexcl;": '\U000000A1', |
"iff;": '\U000021D4', |
"ifr;": '\U0001D526', |
"igrave;": '\U000000EC', |
"ii;": '\U00002148', |
"iiiint;": '\U00002A0C', |
"iiint;": '\U0000222D', |
"iinfin;": '\U000029DC', |
"iiota;": '\U00002129', |
"ijlig;": '\U00000133', |
"imacr;": '\U0000012B', |
"image;": '\U00002111', |
"imagline;": '\U00002110', |
"imagpart;": '\U00002111', |
"imath;": '\U00000131', |
"imof;": '\U000022B7', |
"imped;": '\U000001B5', |
"in;": '\U00002208', |
"incare;": '\U00002105', |
"infin;": '\U0000221E', |
"infintie;": '\U000029DD', |
"inodot;": '\U00000131', |
"int;": '\U0000222B', |
"intcal;": '\U000022BA', |
"integers;": '\U00002124', |
"intercal;": '\U000022BA', |
"intlarhk;": '\U00002A17', |
"intprod;": '\U00002A3C', |
"iocy;": '\U00000451', |
"iogon;": '\U0000012F', |
"iopf;": '\U0001D55A', |
"iota;": '\U000003B9', |
"iprod;": '\U00002A3C', |
"iquest;": '\U000000BF', |
"iscr;": '\U0001D4BE', |
"isin;": '\U00002208', |
"isinE;": '\U000022F9', |
"isindot;": '\U000022F5', |
"isins;": '\U000022F4', |
"isinsv;": '\U000022F3', |
"isinv;": '\U00002208', |
"it;": '\U00002062', |
"itilde;": '\U00000129', |
"iukcy;": '\U00000456', |
"iuml;": '\U000000EF', |
"jcirc;": '\U00000135', |
"jcy;": '\U00000439', |
"jfr;": '\U0001D527', |
"jmath;": '\U00000237', |
"jopf;": '\U0001D55B', |
"jscr;": '\U0001D4BF', |
"jsercy;": '\U00000458', |
"jukcy;": '\U00000454', |
"kappa;": '\U000003BA', |
"kappav;": '\U000003F0', |
"kcedil;": '\U00000137', |
"kcy;": '\U0000043A', |
"kfr;": '\U0001D528', |
"kgreen;": '\U00000138', |
"khcy;": '\U00000445', |
"kjcy;": '\U0000045C', |
"kopf;": '\U0001D55C', |
"kscr;": '\U0001D4C0', |
"lAarr;": '\U000021DA', |
"lArr;": '\U000021D0', |
"lAtail;": '\U0000291B', |
"lBarr;": '\U0000290E', |
"lE;": '\U00002266', |
"lEg;": '\U00002A8B', |
"lHar;": '\U00002962', |
"lacute;": '\U0000013A', |
"laemptyv;": '\U000029B4', |
"lagran;": '\U00002112', |
"lambda;": '\U000003BB', |
"lang;": '\U000027E8', |
"langd;": '\U00002991', |
"langle;": '\U000027E8', |
"lap;": '\U00002A85', |
"laquo;": '\U000000AB', |
"larr;": '\U00002190', |
"larrb;": '\U000021E4', |
"larrbfs;": '\U0000291F', |
"larrfs;": '\U0000291D', |
"larrhk;": '\U000021A9', |
"larrlp;": '\U000021AB', |
"larrpl;": '\U00002939', |
"larrsim;": '\U00002973', |
"larrtl;": '\U000021A2', |
"lat;": '\U00002AAB', |
"latail;": '\U00002919', |
"late;": '\U00002AAD', |
"lbarr;": '\U0000290C', |
"lbbrk;": '\U00002772', |
"lbrace;": '\U0000007B', |
"lbrack;": '\U0000005B', |
"lbrke;": '\U0000298B', |
"lbrksld;": '\U0000298F', |
"lbrkslu;": '\U0000298D', |
"lcaron;": '\U0000013E', |
"lcedil;": '\U0000013C', |
"lceil;": '\U00002308', |
"lcub;": '\U0000007B', |
"lcy;": '\U0000043B', |
"ldca;": '\U00002936', |
"ldquo;": '\U0000201C', |
"ldquor;": '\U0000201E', |
"ldrdhar;": '\U00002967', |
"ldrushar;": '\U0000294B', |
"ldsh;": '\U000021B2', |
"le;": '\U00002264', |
"leftarrow;": '\U00002190', |
"leftarrowtail;": '\U000021A2', |
"leftharpoondown;": '\U000021BD', |
"leftharpoonup;": '\U000021BC', |
"leftleftarrows;": '\U000021C7', |
"leftrightarrow;": '\U00002194', |
"leftrightarrows;": '\U000021C6', |
"leftrightharpoons;": '\U000021CB', |
"leftrightsquigarrow;": '\U000021AD', |
"leftthreetimes;": '\U000022CB', |
"leg;": '\U000022DA', |
"leq;": '\U00002264', |
"leqq;": '\U00002266', |
"leqslant;": '\U00002A7D', |
"les;": '\U00002A7D', |
"lescc;": '\U00002AA8', |
"lesdot;": '\U00002A7F', |
"lesdoto;": '\U00002A81', |
"lesdotor;": '\U00002A83', |
"lesges;": '\U00002A93', |
"lessapprox;": '\U00002A85', |
"lessdot;": '\U000022D6', |
"lesseqgtr;": '\U000022DA', |
"lesseqqgtr;": '\U00002A8B', |
"lessgtr;": '\U00002276', |
"lesssim;": '\U00002272', |
"lfisht;": '\U0000297C', |
"lfloor;": '\U0000230A', |
"lfr;": '\U0001D529', |
"lg;": '\U00002276', |
"lgE;": '\U00002A91', |
"lhard;": '\U000021BD', |
"lharu;": '\U000021BC', |
"lharul;": '\U0000296A', |
"lhblk;": '\U00002584', |
"ljcy;": '\U00000459', |
"ll;": '\U0000226A', |
"llarr;": '\U000021C7', |
"llcorner;": '\U0000231E', |
"llhard;": '\U0000296B', |
"lltri;": '\U000025FA', |
"lmidot;": '\U00000140', |
"lmoust;": '\U000023B0', |
"lmoustache;": '\U000023B0', |
"lnE;": '\U00002268', |
"lnap;": '\U00002A89', |
"lnapprox;": '\U00002A89', |
"lne;": '\U00002A87', |
"lneq;": '\U00002A87', |
"lneqq;": '\U00002268', |
"lnsim;": '\U000022E6', |
"loang;": '\U000027EC', |
"loarr;": '\U000021FD', |
"lobrk;": '\U000027E6', |
"longleftarrow;": '\U000027F5', |
"longleftrightarrow;": '\U000027F7', |
"longmapsto;": '\U000027FC', |
"longrightarrow;": '\U000027F6', |
"looparrowleft;": '\U000021AB', |
"looparrowright;": '\U000021AC', |
"lopar;": '\U00002985', |
"lopf;": '\U0001D55D', |
"loplus;": '\U00002A2D', |
"lotimes;": '\U00002A34', |
"lowast;": '\U00002217', |
"lowbar;": '\U0000005F', |
"loz;": '\U000025CA', |
"lozenge;": '\U000025CA', |
"lozf;": '\U000029EB', |
"lpar;": '\U00000028', |
"lparlt;": '\U00002993', |
"lrarr;": '\U000021C6', |
"lrcorner;": '\U0000231F', |
"lrhar;": '\U000021CB', |
"lrhard;": '\U0000296D', |
"lrm;": '\U0000200E', |
"lrtri;": '\U000022BF', |
"lsaquo;": '\U00002039', |
"lscr;": '\U0001D4C1', |
"lsh;": '\U000021B0', |
"lsim;": '\U00002272', |
"lsime;": '\U00002A8D', |
"lsimg;": '\U00002A8F', |
"lsqb;": '\U0000005B', |
"lsquo;": '\U00002018', |
"lsquor;": '\U0000201A', |
"lstrok;": '\U00000142', |
"lt;": '\U0000003C', |
"ltcc;": '\U00002AA6', |
"ltcir;": '\U00002A79', |
"ltdot;": '\U000022D6', |
"lthree;": '\U000022CB', |
"ltimes;": '\U000022C9', |
"ltlarr;": '\U00002976', |
"ltquest;": '\U00002A7B', |
"ltrPar;": '\U00002996', |
"ltri;": '\U000025C3', |
"ltrie;": '\U000022B4', |
"ltrif;": '\U000025C2', |
"lurdshar;": '\U0000294A', |
"luruhar;": '\U00002966', |
"mDDot;": '\U0000223A', |
"macr;": '\U000000AF', |
"male;": '\U00002642', |
"malt;": '\U00002720', |
"maltese;": '\U00002720', |
"map;": '\U000021A6', |
"mapsto;": '\U000021A6', |
"mapstodown;": '\U000021A7', |
"mapstoleft;": '\U000021A4', |
"mapstoup;": '\U000021A5', |
"marker;": '\U000025AE', |
"mcomma;": '\U00002A29', |
"mcy;": '\U0000043C', |
"mdash;": '\U00002014', |
"measuredangle;": '\U00002221', |
"mfr;": '\U0001D52A', |
"mho;": '\U00002127', |
"micro;": '\U000000B5', |
"mid;": '\U00002223', |
"midast;": '\U0000002A', |
"midcir;": '\U00002AF0', |
"middot;": '\U000000B7', |
"minus;": '\U00002212', |
"minusb;": '\U0000229F', |
"minusd;": '\U00002238', |
"minusdu;": '\U00002A2A', |
"mlcp;": '\U00002ADB', |
"mldr;": '\U00002026', |
"mnplus;": '\U00002213', |
"models;": '\U000022A7', |
"mopf;": '\U0001D55E', |
"mp;": '\U00002213', |
"mscr;": '\U0001D4C2', |
"mstpos;": '\U0000223E', |
"mu;": '\U000003BC', |
"multimap;": '\U000022B8', |
"mumap;": '\U000022B8', |
"nLeftarrow;": '\U000021CD', |
"nLeftrightarrow;": '\U000021CE', |
"nRightarrow;": '\U000021CF', |
"nVDash;": '\U000022AF', |
"nVdash;": '\U000022AE', |
"nabla;": '\U00002207', |
"nacute;": '\U00000144', |
"nap;": '\U00002249', |
"napos;": '\U00000149', |
"napprox;": '\U00002249', |
"natur;": '\U0000266E', |
"natural;": '\U0000266E', |
"naturals;": '\U00002115', |
"nbsp;": '\U000000A0', |
"ncap;": '\U00002A43', |
"ncaron;": '\U00000148', |
"ncedil;": '\U00000146', |
"ncong;": '\U00002247', |
"ncup;": '\U00002A42', |
"ncy;": '\U0000043D', |
"ndash;": '\U00002013', |
"ne;": '\U00002260', |
"neArr;": '\U000021D7', |
"nearhk;": '\U00002924', |
"nearr;": '\U00002197', |
"nearrow;": '\U00002197', |
"nequiv;": '\U00002262', |
"nesear;": '\U00002928', |
"nexist;": '\U00002204', |
"nexists;": '\U00002204', |
"nfr;": '\U0001D52B', |
"nge;": '\U00002271', |
"ngeq;": '\U00002271', |
"ngsim;": '\U00002275', |
"ngt;": '\U0000226F', |
"ngtr;": '\U0000226F', |
"nhArr;": '\U000021CE', |
"nharr;": '\U000021AE', |
"nhpar;": '\U00002AF2', |
"ni;": '\U0000220B', |
"nis;": '\U000022FC', |
"nisd;": '\U000022FA', |
"niv;": '\U0000220B', |
"njcy;": '\U0000045A', |
"nlArr;": '\U000021CD', |
"nlarr;": '\U0000219A', |
"nldr;": '\U00002025', |
"nle;": '\U00002270', |
"nleftarrow;": '\U0000219A', |
"nleftrightarrow;": '\U000021AE', |
"nleq;": '\U00002270', |
"nless;": '\U0000226E', |
"nlsim;": '\U00002274', |
"nlt;": '\U0000226E', |
"nltri;": '\U000022EA', |
"nltrie;": '\U000022EC', |
"nmid;": '\U00002224', |
"nopf;": '\U0001D55F', |
"not;": '\U000000AC', |
"notin;": '\U00002209', |
"notinva;": '\U00002209', |
"notinvb;": '\U000022F7', |
"notinvc;": '\U000022F6', |
"notni;": '\U0000220C', |
"notniva;": '\U0000220C', |
"notnivb;": '\U000022FE', |
"notnivc;": '\U000022FD', |
"npar;": '\U00002226', |
"nparallel;": '\U00002226', |
"npolint;": '\U00002A14', |
"npr;": '\U00002280', |
"nprcue;": '\U000022E0', |
"nprec;": '\U00002280', |
"nrArr;": '\U000021CF', |
"nrarr;": '\U0000219B', |
"nrightarrow;": '\U0000219B', |
"nrtri;": '\U000022EB', |
"nrtrie;": '\U000022ED', |
"nsc;": '\U00002281', |
"nsccue;": '\U000022E1', |
"nscr;": '\U0001D4C3', |
"nshortmid;": '\U00002224', |
"nshortparallel;": '\U00002226', |
"nsim;": '\U00002241', |
"nsime;": '\U00002244', |
"nsimeq;": '\U00002244', |
"nsmid;": '\U00002224', |
"nspar;": '\U00002226', |
"nsqsube;": '\U000022E2', |
"nsqsupe;": '\U000022E3', |
"nsub;": '\U00002284', |
"nsube;": '\U00002288', |
"nsubseteq;": '\U00002288', |
"nsucc;": '\U00002281', |
"nsup;": '\U00002285', |
"nsupe;": '\U00002289', |
"nsupseteq;": '\U00002289', |
"ntgl;": '\U00002279', |
"ntilde;": '\U000000F1', |
"ntlg;": '\U00002278', |
"ntriangleleft;": '\U000022EA', |
"ntrianglelefteq;": '\U000022EC', |
"ntriangleright;": '\U000022EB', |
"ntrianglerighteq;": '\U000022ED', |
"nu;": '\U000003BD', |
"num;": '\U00000023', |
"numero;": '\U00002116', |
"numsp;": '\U00002007', |
"nvDash;": '\U000022AD', |
"nvHarr;": '\U00002904', |
"nvdash;": '\U000022AC', |
"nvinfin;": '\U000029DE', |
"nvlArr;": '\U00002902', |
"nvrArr;": '\U00002903', |
"nwArr;": '\U000021D6', |
"nwarhk;": '\U00002923', |
"nwarr;": '\U00002196', |
"nwarrow;": '\U00002196', |
"nwnear;": '\U00002927', |
"oS;": '\U000024C8', |
"oacute;": '\U000000F3', |
"oast;": '\U0000229B', |
"ocir;": '\U0000229A', |
"ocirc;": '\U000000F4', |
"ocy;": '\U0000043E', |
"odash;": '\U0000229D', |
"odblac;": '\U00000151', |
"odiv;": '\U00002A38', |
"odot;": '\U00002299', |
"odsold;": '\U000029BC', |
"oelig;": '\U00000153', |
"ofcir;": '\U000029BF', |
"ofr;": '\U0001D52C', |
"ogon;": '\U000002DB', |
"ograve;": '\U000000F2', |
"ogt;": '\U000029C1', |
"ohbar;": '\U000029B5', |
"ohm;": '\U000003A9', |
"oint;": '\U0000222E', |
"olarr;": '\U000021BA', |
"olcir;": '\U000029BE', |
"olcross;": '\U000029BB', |
"oline;": '\U0000203E', |
"olt;": '\U000029C0', |
"omacr;": '\U0000014D', |
"omega;": '\U000003C9', |
"omicron;": '\U000003BF', |
"omid;": '\U000029B6', |
"ominus;": '\U00002296', |
"oopf;": '\U0001D560', |
"opar;": '\U000029B7', |
"operp;": '\U000029B9', |
"oplus;": '\U00002295', |
"or;": '\U00002228', |
"orarr;": '\U000021BB', |
"ord;": '\U00002A5D', |
"order;": '\U00002134', |
"orderof;": '\U00002134', |
"ordf;": '\U000000AA', |
"ordm;": '\U000000BA', |
"origof;": '\U000022B6', |
"oror;": '\U00002A56', |
"orslope;": '\U00002A57', |
"orv;": '\U00002A5B', |
"oscr;": '\U00002134', |
"oslash;": '\U000000F8', |
"osol;": '\U00002298', |
"otilde;": '\U000000F5', |
"otimes;": '\U00002297', |
"otimesas;": '\U00002A36', |
"ouml;": '\U000000F6', |
"ovbar;": '\U0000233D', |
"par;": '\U00002225', |
"para;": '\U000000B6', |
"parallel;": '\U00002225', |
"parsim;": '\U00002AF3', |
"parsl;": '\U00002AFD', |
"part;": '\U00002202', |
"pcy;": '\U0000043F', |
"percnt;": '\U00000025', |
"period;": '\U0000002E', |
"permil;": '\U00002030', |
"perp;": '\U000022A5', |
"pertenk;": '\U00002031', |
"pfr;": '\U0001D52D', |
"phi;": '\U000003C6', |
"phiv;": '\U000003D5', |
"phmmat;": '\U00002133', |
"phone;": '\U0000260E', |
"pi;": '\U000003C0', |
"pitchfork;": '\U000022D4', |
"piv;": '\U000003D6', |
"planck;": '\U0000210F', |
"planckh;": '\U0000210E', |
"plankv;": '\U0000210F', |
"plus;": '\U0000002B', |
"plusacir;": '\U00002A23', |
"plusb;": '\U0000229E', |
"pluscir;": '\U00002A22', |
"plusdo;": '\U00002214', |
"plusdu;": '\U00002A25', |
"pluse;": '\U00002A72', |
"plusmn;": '\U000000B1', |
"plussim;": '\U00002A26', |
"plustwo;": '\U00002A27', |
"pm;": '\U000000B1', |
"pointint;": '\U00002A15', |
"popf;": '\U0001D561', |
"pound;": '\U000000A3', |
"pr;": '\U0000227A', |
"prE;": '\U00002AB3', |
"prap;": '\U00002AB7', |
"prcue;": '\U0000227C', |
"pre;": '\U00002AAF', |
"prec;": '\U0000227A', |
"precapprox;": '\U00002AB7', |
"preccurlyeq;": '\U0000227C', |
"preceq;": '\U00002AAF', |
"precnapprox;": '\U00002AB9', |
"precneqq;": '\U00002AB5', |
"precnsim;": '\U000022E8', |
"precsim;": '\U0000227E', |
"prime;": '\U00002032', |
"primes;": '\U00002119', |
"prnE;": '\U00002AB5', |
"prnap;": '\U00002AB9', |
"prnsim;": '\U000022E8', |
"prod;": '\U0000220F', |
"profalar;": '\U0000232E', |
"profline;": '\U00002312', |
"profsurf;": '\U00002313', |
"prop;": '\U0000221D', |
"propto;": '\U0000221D', |
"prsim;": '\U0000227E', |
"prurel;": '\U000022B0', |
"pscr;": '\U0001D4C5', |
"psi;": '\U000003C8', |
"puncsp;": '\U00002008', |
"qfr;": '\U0001D52E', |
"qint;": '\U00002A0C', |
"qopf;": '\U0001D562', |
"qprime;": '\U00002057', |
"qscr;": '\U0001D4C6', |
"quaternions;": '\U0000210D', |
"quatint;": '\U00002A16', |
"quest;": '\U0000003F', |
"questeq;": '\U0000225F', |
"quot;": '\U00000022', |
"rAarr;": '\U000021DB', |
"rArr;": '\U000021D2', |
"rAtail;": '\U0000291C', |
"rBarr;": '\U0000290F', |
"rHar;": '\U00002964', |
"racute;": '\U00000155', |
"radic;": '\U0000221A', |
"raemptyv;": '\U000029B3', |
"rang;": '\U000027E9', |
"rangd;": '\U00002992', |
"range;": '\U000029A5', |
"rangle;": '\U000027E9', |
"raquo;": '\U000000BB', |
"rarr;": '\U00002192', |
"rarrap;": '\U00002975', |
"rarrb;": '\U000021E5', |
"rarrbfs;": '\U00002920', |
"rarrc;": '\U00002933', |
"rarrfs;": '\U0000291E', |
"rarrhk;": '\U000021AA', |
"rarrlp;": '\U000021AC', |
"rarrpl;": '\U00002945', |
"rarrsim;": '\U00002974', |
"rarrtl;": '\U000021A3', |
"rarrw;": '\U0000219D', |
"ratail;": '\U0000291A', |
"ratio;": '\U00002236', |
"rationals;": '\U0000211A', |
"rbarr;": '\U0000290D', |
"rbbrk;": '\U00002773', |
"rbrace;": '\U0000007D', |
"rbrack;": '\U0000005D', |
"rbrke;": '\U0000298C', |
"rbrksld;": '\U0000298E', |
"rbrkslu;": '\U00002990', |
"rcaron;": '\U00000159', |
"rcedil;": '\U00000157', |
"rceil;": '\U00002309', |
"rcub;": '\U0000007D', |
"rcy;": '\U00000440', |
"rdca;": '\U00002937', |
"rdldhar;": '\U00002969', |
"rdquo;": '\U0000201D', |
"rdquor;": '\U0000201D', |
"rdsh;": '\U000021B3', |
"real;": '\U0000211C', |
"realine;": '\U0000211B', |
"realpart;": '\U0000211C', |
"reals;": '\U0000211D', |
"rect;": '\U000025AD', |
"reg;": '\U000000AE', |
"rfisht;": '\U0000297D', |
"rfloor;": '\U0000230B', |
"rfr;": '\U0001D52F', |
"rhard;": '\U000021C1', |
"rharu;": '\U000021C0', |
"rharul;": '\U0000296C', |
"rho;": '\U000003C1', |
"rhov;": '\U000003F1', |
"rightarrow;": '\U00002192', |
"rightarrowtail;": '\U000021A3', |
"rightharpoondown;": '\U000021C1', |
"rightharpoonup;": '\U000021C0', |
"rightleftarrows;": '\U000021C4', |
"rightleftharpoons;": '\U000021CC', |
"rightrightarrows;": '\U000021C9', |
"rightsquigarrow;": '\U0000219D', |
"rightthreetimes;": '\U000022CC', |
"ring;": '\U000002DA', |
"risingdotseq;": '\U00002253', |
"rlarr;": '\U000021C4', |
"rlhar;": '\U000021CC', |
"rlm;": '\U0000200F', |
"rmoust;": '\U000023B1', |
"rmoustache;": '\U000023B1', |
"rnmid;": '\U00002AEE', |
"roang;": '\U000027ED', |
"roarr;": '\U000021FE', |
"robrk;": '\U000027E7', |
"ropar;": '\U00002986', |
"ropf;": '\U0001D563', |
"roplus;": '\U00002A2E', |
"rotimes;": '\U00002A35', |
"rpar;": '\U00000029', |
"rpargt;": '\U00002994', |
"rppolint;": '\U00002A12', |
"rrarr;": '\U000021C9', |
"rsaquo;": '\U0000203A', |
"rscr;": '\U0001D4C7', |
"rsh;": '\U000021B1', |
"rsqb;": '\U0000005D', |
"rsquo;": '\U00002019', |
"rsquor;": '\U00002019', |
"rthree;": '\U000022CC', |
"rtimes;": '\U000022CA', |
"rtri;": '\U000025B9', |
"rtrie;": '\U000022B5', |
"rtrif;": '\U000025B8', |
"rtriltri;": '\U000029CE', |
"ruluhar;": '\U00002968', |
"rx;": '\U0000211E', |
"sacute;": '\U0000015B', |
"sbquo;": '\U0000201A', |
"sc;": '\U0000227B', |
"scE;": '\U00002AB4', |
"scap;": '\U00002AB8', |
"scaron;": '\U00000161', |
"sccue;": '\U0000227D', |
"sce;": '\U00002AB0', |
"scedil;": '\U0000015F', |
"scirc;": '\U0000015D', |
"scnE;": '\U00002AB6', |
"scnap;": '\U00002ABA', |
"scnsim;": '\U000022E9', |
"scpolint;": '\U00002A13', |
"scsim;": '\U0000227F', |
"scy;": '\U00000441', |
"sdot;": '\U000022C5', |
"sdotb;": '\U000022A1', |
"sdote;": '\U00002A66', |
"seArr;": '\U000021D8', |
"searhk;": '\U00002925', |
"searr;": '\U00002198', |
"searrow;": '\U00002198', |
"sect;": '\U000000A7', |
"semi;": '\U0000003B', |
"seswar;": '\U00002929', |
"setminus;": '\U00002216', |
"setmn;": '\U00002216', |
"sext;": '\U00002736', |
"sfr;": '\U0001D530', |
"sfrown;": '\U00002322', |
"sharp;": '\U0000266F', |
"shchcy;": '\U00000449', |
"shcy;": '\U00000448', |
"shortmid;": '\U00002223', |
"shortparallel;": '\U00002225', |
"shy;": '\U000000AD', |
"sigma;": '\U000003C3', |
"sigmaf;": '\U000003C2', |
"sigmav;": '\U000003C2', |
"sim;": '\U0000223C', |
"simdot;": '\U00002A6A', |
"sime;": '\U00002243', |
"simeq;": '\U00002243', |
"simg;": '\U00002A9E', |
"simgE;": '\U00002AA0', |
"siml;": '\U00002A9D', |
"simlE;": '\U00002A9F', |
"simne;": '\U00002246', |
"simplus;": '\U00002A24', |
"simrarr;": '\U00002972', |
"slarr;": '\U00002190', |
"smallsetminus;": '\U00002216', |
"smashp;": '\U00002A33', |
"smeparsl;": '\U000029E4', |
"smid;": '\U00002223', |
"smile;": '\U00002323', |
"smt;": '\U00002AAA', |
"smte;": '\U00002AAC', |
"softcy;": '\U0000044C', |
"sol;": '\U0000002F', |
"solb;": '\U000029C4', |
"solbar;": '\U0000233F', |
"sopf;": '\U0001D564', |
"spades;": '\U00002660', |
"spadesuit;": '\U00002660', |
"spar;": '\U00002225', |
"sqcap;": '\U00002293', |
"sqcup;": '\U00002294', |
"sqsub;": '\U0000228F', |
"sqsube;": '\U00002291', |
"sqsubset;": '\U0000228F', |
"sqsubseteq;": '\U00002291', |
"sqsup;": '\U00002290', |
"sqsupe;": '\U00002292', |
"sqsupset;": '\U00002290', |
"sqsupseteq;": '\U00002292', |
"squ;": '\U000025A1', |
"square;": '\U000025A1', |
"squarf;": '\U000025AA', |
"squf;": '\U000025AA', |
"srarr;": '\U00002192', |
"sscr;": '\U0001D4C8', |
"ssetmn;": '\U00002216', |
"ssmile;": '\U00002323', |
"sstarf;": '\U000022C6', |
"star;": '\U00002606', |
"starf;": '\U00002605', |
"straightepsilon;": '\U000003F5', |
"straightphi;": '\U000003D5', |
"strns;": '\U000000AF', |
"sub;": '\U00002282', |
"subE;": '\U00002AC5', |
"subdot;": '\U00002ABD', |
"sube;": '\U00002286', |
"subedot;": '\U00002AC3', |
"submult;": '\U00002AC1', |
"subnE;": '\U00002ACB', |
"subne;": '\U0000228A', |
"subplus;": '\U00002ABF', |
"subrarr;": '\U00002979', |
"subset;": '\U00002282', |
"subseteq;": '\U00002286', |
"subseteqq;": '\U00002AC5', |
"subsetneq;": '\U0000228A', |
"subsetneqq;": '\U00002ACB', |
"subsim;": '\U00002AC7', |
"subsub;": '\U00002AD5', |
"subsup;": '\U00002AD3', |
"succ;": '\U0000227B', |
"succapprox;": '\U00002AB8', |
"succcurlyeq;": '\U0000227D', |
"succeq;": '\U00002AB0', |
"succnapprox;": '\U00002ABA', |
"succneqq;": '\U00002AB6', |
"succnsim;": '\U000022E9', |
"succsim;": '\U0000227F', |
"sum;": '\U00002211', |
"sung;": '\U0000266A', |
"sup;": '\U00002283', |
"sup1;": '\U000000B9', |
"sup2;": '\U000000B2', |
"sup3;": '\U000000B3', |
"supE;": '\U00002AC6', |
"supdot;": '\U00002ABE', |
"supdsub;": '\U00002AD8', |
"supe;": '\U00002287', |
"supedot;": '\U00002AC4', |
"suphsol;": '\U000027C9', |
"suphsub;": '\U00002AD7', |
"suplarr;": '\U0000297B', |
"supmult;": '\U00002AC2', |
"supnE;": '\U00002ACC', |
"supne;": '\U0000228B', |
"supplus;": '\U00002AC0', |
"supset;": '\U00002283', |
"supseteq;": '\U00002287', |
"supseteqq;": '\U00002AC6', |
"supsetneq;": '\U0000228B', |
"supsetneqq;": '\U00002ACC', |
"supsim;": '\U00002AC8', |
"supsub;": '\U00002AD4', |
"supsup;": '\U00002AD6', |
"swArr;": '\U000021D9', |
"swarhk;": '\U00002926', |
"swarr;": '\U00002199', |
"swarrow;": '\U00002199', |
"swnwar;": '\U0000292A', |
"szlig;": '\U000000DF', |
"target;": '\U00002316', |
"tau;": '\U000003C4', |
"tbrk;": '\U000023B4', |
"tcaron;": '\U00000165', |
"tcedil;": '\U00000163', |
"tcy;": '\U00000442', |
"tdot;": '\U000020DB', |
"telrec;": '\U00002315', |
"tfr;": '\U0001D531', |
"there4;": '\U00002234', |
"therefore;": '\U00002234', |
"theta;": '\U000003B8', |
"thetasym;": '\U000003D1', |
"thetav;": '\U000003D1', |
"thickapprox;": '\U00002248', |
"thicksim;": '\U0000223C', |
"thinsp;": '\U00002009', |
"thkap;": '\U00002248', |
"thksim;": '\U0000223C', |
"thorn;": '\U000000FE', |
"tilde;": '\U000002DC', |
"times;": '\U000000D7', |
"timesb;": '\U000022A0', |
"timesbar;": '\U00002A31', |
"timesd;": '\U00002A30', |
"tint;": '\U0000222D', |
"toea;": '\U00002928', |
"top;": '\U000022A4', |
"topbot;": '\U00002336', |
"topcir;": '\U00002AF1', |
"topf;": '\U0001D565', |
"topfork;": '\U00002ADA', |
"tosa;": '\U00002929', |
"tprime;": '\U00002034', |
"trade;": '\U00002122', |
"triangle;": '\U000025B5', |
"triangledown;": '\U000025BF', |
"triangleleft;": '\U000025C3', |
"trianglelefteq;": '\U000022B4', |
"triangleq;": '\U0000225C', |
"triangleright;": '\U000025B9', |
"trianglerighteq;": '\U000022B5', |
"tridot;": '\U000025EC', |
"trie;": '\U0000225C', |
"triminus;": '\U00002A3A', |
"triplus;": '\U00002A39', |
"trisb;": '\U000029CD', |
"tritime;": '\U00002A3B', |
"trpezium;": '\U000023E2', |
"tscr;": '\U0001D4C9', |
"tscy;": '\U00000446', |
"tshcy;": '\U0000045B', |
"tstrok;": '\U00000167', |
"twixt;": '\U0000226C', |
"twoheadleftarrow;": '\U0000219E', |
"twoheadrightarrow;": '\U000021A0', |
"uArr;": '\U000021D1', |
"uHar;": '\U00002963', |
"uacute;": '\U000000FA', |
"uarr;": '\U00002191', |
"ubrcy;": '\U0000045E', |
"ubreve;": '\U0000016D', |
"ucirc;": '\U000000FB', |
"ucy;": '\U00000443', |
"udarr;": '\U000021C5', |
"udblac;": '\U00000171', |
"udhar;": '\U0000296E', |
"ufisht;": '\U0000297E', |
"ufr;": '\U0001D532', |
"ugrave;": '\U000000F9', |
"uharl;": '\U000021BF', |
"uharr;": '\U000021BE', |
"uhblk;": '\U00002580', |
"ulcorn;": '\U0000231C', |
"ulcorner;": '\U0000231C', |
"ulcrop;": '\U0000230F', |
"ultri;": '\U000025F8', |
"umacr;": '\U0000016B', |
"uml;": '\U000000A8', |
"uogon;": '\U00000173', |
"uopf;": '\U0001D566', |
"uparrow;": '\U00002191', |
"updownarrow;": '\U00002195', |
"upharpoonleft;": '\U000021BF', |
"upharpoonright;": '\U000021BE', |
"uplus;": '\U0000228E', |
"upsi;": '\U000003C5', |
"upsih;": '\U000003D2', |
"upsilon;": '\U000003C5', |
"upuparrows;": '\U000021C8', |
"urcorn;": '\U0000231D', |
"urcorner;": '\U0000231D', |
"urcrop;": '\U0000230E', |
"uring;": '\U0000016F', |
"urtri;": '\U000025F9', |
"uscr;": '\U0001D4CA', |
"utdot;": '\U000022F0', |
"utilde;": '\U00000169', |
"utri;": '\U000025B5', |
"utrif;": '\U000025B4', |
"uuarr;": '\U000021C8', |
"uuml;": '\U000000FC', |
"uwangle;": '\U000029A7', |
"vArr;": '\U000021D5', |
"vBar;": '\U00002AE8', |
"vBarv;": '\U00002AE9', |
"vDash;": '\U000022A8', |
"vangrt;": '\U0000299C', |
"varepsilon;": '\U000003F5', |
"varkappa;": '\U000003F0', |
"varnothing;": '\U00002205', |
"varphi;": '\U000003D5', |
"varpi;": '\U000003D6', |
"varpropto;": '\U0000221D', |
"varr;": '\U00002195', |
"varrho;": '\U000003F1', |
"varsigma;": '\U000003C2', |
"vartheta;": '\U000003D1', |
"vartriangleleft;": '\U000022B2', |
"vartriangleright;": '\U000022B3', |
"vcy;": '\U00000432', |
"vdash;": '\U000022A2', |
"vee;": '\U00002228', |
"veebar;": '\U000022BB', |
"veeeq;": '\U0000225A', |
"vellip;": '\U000022EE', |
"verbar;": '\U0000007C', |
"vert;": '\U0000007C', |
"vfr;": '\U0001D533', |
"vltri;": '\U000022B2', |
"vopf;": '\U0001D567', |
"vprop;": '\U0000221D', |
"vrtri;": '\U000022B3', |
"vscr;": '\U0001D4CB', |
"vzigzag;": '\U0000299A', |
"wcirc;": '\U00000175', |
"wedbar;": '\U00002A5F', |
"wedge;": '\U00002227', |
"wedgeq;": '\U00002259', |
"weierp;": '\U00002118', |
"wfr;": '\U0001D534', |
"wopf;": '\U0001D568', |
"wp;": '\U00002118', |
"wr;": '\U00002240', |
"wreath;": '\U00002240', |
"wscr;": '\U0001D4CC', |
"xcap;": '\U000022C2', |
"xcirc;": '\U000025EF', |
"xcup;": '\U000022C3', |
"xdtri;": '\U000025BD', |
"xfr;": '\U0001D535', |
"xhArr;": '\U000027FA', |
"xharr;": '\U000027F7', |
"xi;": '\U000003BE', |
"xlArr;": '\U000027F8', |
"xlarr;": '\U000027F5', |
"xmap;": '\U000027FC', |
"xnis;": '\U000022FB', |
"xodot;": '\U00002A00', |
"xopf;": '\U0001D569', |
"xoplus;": '\U00002A01', |
"xotime;": '\U00002A02', |
"xrArr;": '\U000027F9', |
"xrarr;": '\U000027F6', |
"xscr;": '\U0001D4CD', |
"xsqcup;": '\U00002A06', |
"xuplus;": '\U00002A04', |
"xutri;": '\U000025B3', |
"xvee;": '\U000022C1', |
"xwedge;": '\U000022C0', |
"yacute;": '\U000000FD', |
"yacy;": '\U0000044F', |
"ycirc;": '\U00000177', |
"ycy;": '\U0000044B', |
"yen;": '\U000000A5', |
"yfr;": '\U0001D536', |
"yicy;": '\U00000457', |
"yopf;": '\U0001D56A', |
"yscr;": '\U0001D4CE', |
"yucy;": '\U0000044E', |
"yuml;": '\U000000FF', |
"zacute;": '\U0000017A', |
"zcaron;": '\U0000017E', |
"zcy;": '\U00000437', |
"zdot;": '\U0000017C', |
"zeetrf;": '\U00002128', |
"zeta;": '\U000003B6', |
"zfr;": '\U0001D537', |
"zhcy;": '\U00000436', |
"zigrarr;": '\U000021DD', |
"zopf;": '\U0001D56B', |
"zscr;": '\U0001D4CF', |
"zwj;": '\U0000200D', |
"zwnj;": '\U0000200C', |
"AElig": '\U000000C6', |
"AMP": '\U00000026', |
"Aacute": '\U000000C1', |
"Acirc": '\U000000C2', |
"Agrave": '\U000000C0', |
"Aring": '\U000000C5', |
"Atilde": '\U000000C3', |
"Auml": '\U000000C4', |
"COPY": '\U000000A9', |
"Ccedil": '\U000000C7', |
"ETH": '\U000000D0', |
"Eacute": '\U000000C9', |
"Ecirc": '\U000000CA', |
"Egrave": '\U000000C8', |
"Euml": '\U000000CB', |
"GT": '\U0000003E', |
"Iacute": '\U000000CD', |
"Icirc": '\U000000CE', |
"Igrave": '\U000000CC', |
"Iuml": '\U000000CF', |
"LT": '\U0000003C', |
"Ntilde": '\U000000D1', |
"Oacute": '\U000000D3', |
"Ocirc": '\U000000D4', |
"Ograve": '\U000000D2', |
"Oslash": '\U000000D8', |
"Otilde": '\U000000D5', |
"Ouml": '\U000000D6', |
"QUOT": '\U00000022', |
"REG": '\U000000AE', |
"THORN": '\U000000DE', |
"Uacute": '\U000000DA', |
"Ucirc": '\U000000DB', |
"Ugrave": '\U000000D9', |
"Uuml": '\U000000DC', |
"Yacute": '\U000000DD', |
"aacute": '\U000000E1', |
"acirc": '\U000000E2', |
"acute": '\U000000B4', |
"aelig": '\U000000E6', |
"agrave": '\U000000E0', |
"amp": '\U00000026', |
"aring": '\U000000E5', |
"atilde": '\U000000E3', |
"auml": '\U000000E4', |
"brvbar": '\U000000A6', |
"ccedil": '\U000000E7', |
"cedil": '\U000000B8', |
"cent": '\U000000A2', |
"copy": '\U000000A9', |
"curren": '\U000000A4', |
"deg": '\U000000B0', |
"divide": '\U000000F7', |
"eacute": '\U000000E9', |
"ecirc": '\U000000EA', |
"egrave": '\U000000E8', |
"eth": '\U000000F0', |
"euml": '\U000000EB', |
"frac12": '\U000000BD', |
"frac14": '\U000000BC', |
"frac34": '\U000000BE', |
"gt": '\U0000003E', |
"iacute": '\U000000ED', |
"icirc": '\U000000EE', |
"iexcl": '\U000000A1', |
"igrave": '\U000000EC', |
"iquest": '\U000000BF', |
"iuml": '\U000000EF', |
"laquo": '\U000000AB', |
"lt": '\U0000003C', |
"macr": '\U000000AF', |
"micro": '\U000000B5', |
"middot": '\U000000B7', |
"nbsp": '\U000000A0', |
"not": '\U000000AC', |
"ntilde": '\U000000F1', |
"oacute": '\U000000F3', |
"ocirc": '\U000000F4', |
"ograve": '\U000000F2', |
"ordf": '\U000000AA', |
"ordm": '\U000000BA', |
"oslash": '\U000000F8', |
"otilde": '\U000000F5', |
"ouml": '\U000000F6', |
"para": '\U000000B6', |
"plusmn": '\U000000B1', |
"pound": '\U000000A3', |
"quot": '\U00000022', |
"raquo": '\U000000BB', |
"reg": '\U000000AE', |
"sect": '\U000000A7', |
"shy": '\U000000AD', |
"sup1": '\U000000B9', |
"sup2": '\U000000B2', |
"sup3": '\U000000B3', |
"szlig": '\U000000DF', |
"thorn": '\U000000FE', |
"times": '\U000000D7', |
"uacute": '\U000000FA', |
"ucirc": '\U000000FB', |
"ugrave": '\U000000F9', |
"uml": '\U000000A8', |
"uuml": '\U000000FC', |
"yacute": '\U000000FD', |
"yen": '\U000000A5', |
"yuml": '\U000000FF', |
} |
|
// HTML entities that are two unicode codepoints. |
var entity2 = map[string][2]rune{ |
// TODO(nigeltao): Handle replacements that are wider than their names. |
// "nLt;": {'\u226A', '\u20D2'}, |
// "nGt;": {'\u226B', '\u20D2'}, |
"NotEqualTilde;": {'\u2242', '\u0338'}, |
"NotGreaterFullEqual;": {'\u2267', '\u0338'}, |
"NotGreaterGreater;": {'\u226B', '\u0338'}, |
"NotGreaterSlantEqual;": {'\u2A7E', '\u0338'}, |
"NotHumpDownHump;": {'\u224E', '\u0338'}, |
"NotHumpEqual;": {'\u224F', '\u0338'}, |
"NotLeftTriangleBar;": {'\u29CF', '\u0338'}, |
"NotLessLess;": {'\u226A', '\u0338'}, |
"NotLessSlantEqual;": {'\u2A7D', '\u0338'}, |
"NotNestedGreaterGreater;": {'\u2AA2', '\u0338'}, |
"NotNestedLessLess;": {'\u2AA1', '\u0338'}, |
"NotPrecedesEqual;": {'\u2AAF', '\u0338'}, |
"NotRightTriangleBar;": {'\u29D0', '\u0338'}, |
"NotSquareSubset;": {'\u228F', '\u0338'}, |
"NotSquareSuperset;": {'\u2290', '\u0338'}, |
"NotSubset;": {'\u2282', '\u20D2'}, |
"NotSucceedsEqual;": {'\u2AB0', '\u0338'}, |
"NotSucceedsTilde;": {'\u227F', '\u0338'}, |
"NotSuperset;": {'\u2283', '\u20D2'}, |
"ThickSpace;": {'\u205F', '\u200A'}, |
"acE;": {'\u223E', '\u0333'}, |
"bne;": {'\u003D', '\u20E5'}, |
"bnequiv;": {'\u2261', '\u20E5'}, |
"caps;": {'\u2229', '\uFE00'}, |
"cups;": {'\u222A', '\uFE00'}, |
"fjlig;": {'\u0066', '\u006A'}, |
"gesl;": {'\u22DB', '\uFE00'}, |
"gvertneqq;": {'\u2269', '\uFE00'}, |
"gvnE;": {'\u2269', '\uFE00'}, |
"lates;": {'\u2AAD', '\uFE00'}, |
"lesg;": {'\u22DA', '\uFE00'}, |
"lvertneqq;": {'\u2268', '\uFE00'}, |
"lvnE;": {'\u2268', '\uFE00'}, |
"nGg;": {'\u22D9', '\u0338'}, |
"nGtv;": {'\u226B', '\u0338'}, |
"nLl;": {'\u22D8', '\u0338'}, |
"nLtv;": {'\u226A', '\u0338'}, |
"nang;": {'\u2220', '\u20D2'}, |
"napE;": {'\u2A70', '\u0338'}, |
"napid;": {'\u224B', '\u0338'}, |
"nbump;": {'\u224E', '\u0338'}, |
"nbumpe;": {'\u224F', '\u0338'}, |
"ncongdot;": {'\u2A6D', '\u0338'}, |
"nedot;": {'\u2250', '\u0338'}, |
"nesim;": {'\u2242', '\u0338'}, |
"ngE;": {'\u2267', '\u0338'}, |
"ngeqq;": {'\u2267', '\u0338'}, |
"ngeqslant;": {'\u2A7E', '\u0338'}, |
"nges;": {'\u2A7E', '\u0338'}, |
"nlE;": {'\u2266', '\u0338'}, |
"nleqq;": {'\u2266', '\u0338'}, |
"nleqslant;": {'\u2A7D', '\u0338'}, |
"nles;": {'\u2A7D', '\u0338'}, |
"notinE;": {'\u22F9', '\u0338'}, |
"notindot;": {'\u22F5', '\u0338'}, |
"nparsl;": {'\u2AFD', '\u20E5'}, |
"npart;": {'\u2202', '\u0338'}, |
"npre;": {'\u2AAF', '\u0338'}, |
"npreceq;": {'\u2AAF', '\u0338'}, |
"nrarrc;": {'\u2933', '\u0338'}, |
"nrarrw;": {'\u219D', '\u0338'}, |
"nsce;": {'\u2AB0', '\u0338'}, |
"nsubE;": {'\u2AC5', '\u0338'}, |
"nsubset;": {'\u2282', '\u20D2'}, |
"nsubseteqq;": {'\u2AC5', '\u0338'}, |
"nsucceq;": {'\u2AB0', '\u0338'}, |
"nsupE;": {'\u2AC6', '\u0338'}, |
"nsupset;": {'\u2283', '\u20D2'}, |
"nsupseteqq;": {'\u2AC6', '\u0338'}, |
"nvap;": {'\u224D', '\u20D2'}, |
"nvge;": {'\u2265', '\u20D2'}, |
"nvgt;": {'\u003E', '\u20D2'}, |
"nvle;": {'\u2264', '\u20D2'}, |
"nvlt;": {'\u003C', '\u20D2'}, |
"nvltrie;": {'\u22B4', '\u20D2'}, |
"nvrtrie;": {'\u22B5', '\u20D2'}, |
"nvsim;": {'\u223C', '\u20D2'}, |
"race;": {'\u223D', '\u0331'}, |
"smtes;": {'\u2AAC', '\uFE00'}, |
"sqcaps;": {'\u2293', '\uFE00'}, |
"sqcups;": {'\u2294', '\uFE00'}, |
"varsubsetneq;": {'\u228A', '\uFE00'}, |
"varsubsetneqq;": {'\u2ACB', '\uFE00'}, |
"varsupsetneq;": {'\u228B', '\uFE00'}, |
"varsupsetneqq;": {'\u2ACC', '\uFE00'}, |
"vnsub;": {'\u2282', '\u20D2'}, |
"vnsup;": {'\u2283', '\u20D2'}, |
"vsubnE;": {'\u2ACB', '\uFE00'}, |
"vsubne;": {'\u228A', '\uFE00'}, |
"vsupnE;": {'\u2ACC', '\uFE00'}, |
"vsupne;": {'\u228B', '\uFE00'}, |
} |
/escape.go
0,0 → 1,258
// Copyright 2010 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
// Package html provides functions for escaping and unescaping HTML text. |
package html |
|
import ( |
"bytes" |
"strings" |
"unicode/utf8" |
) |
|
type writer interface { |
WriteString(string) (int, error) |
} |
|
// These replacements permit compatibility with old numeric entities that |
// assumed Windows-1252 encoding. |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference |
var replacementTable = [...]rune{ |
'\u20AC', // First entry is what 0x80 should be replaced with. |
'\u0081', |
'\u201A', |
'\u0192', |
'\u201E', |
'\u2026', |
'\u2020', |
'\u2021', |
'\u02C6', |
'\u2030', |
'\u0160', |
'\u2039', |
'\u0152', |
'\u008D', |
'\u017D', |
'\u008F', |
'\u0090', |
'\u2018', |
'\u2019', |
'\u201C', |
'\u201D', |
'\u2022', |
'\u2013', |
'\u2014', |
'\u02DC', |
'\u2122', |
'\u0161', |
'\u203A', |
'\u0153', |
'\u009D', |
'\u017E', |
'\u0178', // Last entry is 0x9F. |
// 0x00->'\uFFFD' is handled programmatically. |
// 0x0D->'\u000D' is a no-op. |
} |
|
// unescapeEntity reads an entity like "<" from b[src:] and writes the |
// corresponding "<" to b[dst:], returning the incremented dst and src cursors. |
// Precondition: b[src] == '&' && dst <= src. |
// attribute should be true if parsing an attribute value. |
func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference |
|
// i starts at 1 because we already know that s[0] == '&'. |
i, s := 1, b[src:] |
|
if len(s) <= 1 { |
b[dst] = b[src] |
return dst + 1, src + 1 |
} |
|
if s[i] == '#' { |
if len(s) <= 3 { // We need to have at least "&#.". |
b[dst] = b[src] |
return dst + 1, src + 1 |
} |
i++ |
c := s[i] |
hex := false |
if c == 'x' || c == 'X' { |
hex = true |
i++ |
} |
|
x := '\x00' |
for i < len(s) { |
c = s[i] |
i++ |
if hex { |
if '0' <= c && c <= '9' { |
x = 16*x + rune(c) - '0' |
continue |
} else if 'a' <= c && c <= 'f' { |
x = 16*x + rune(c) - 'a' + 10 |
continue |
} else if 'A' <= c && c <= 'F' { |
x = 16*x + rune(c) - 'A' + 10 |
continue |
} |
} else if '0' <= c && c <= '9' { |
x = 10*x + rune(c) - '0' |
continue |
} |
if c != ';' { |
i-- |
} |
break |
} |
|
if i <= 3 { // No characters matched. |
b[dst] = b[src] |
return dst + 1, src + 1 |
} |
|
if 0x80 <= x && x <= 0x9F { |
// Replace characters from Windows-1252 with UTF-8 equivalents. |
x = replacementTable[x-0x80] |
} else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF { |
// Replace invalid characters with the replacement character. |
x = '\uFFFD' |
} |
|
return dst + utf8.EncodeRune(b[dst:], x), src + i |
} |
|
// Consume the maximum number of characters possible, with the |
// consumed characters matching one of the named references. |
|
for i < len(s) { |
c := s[i] |
i++ |
// Lower-cased characters are more common in entities, so we check for them first. |
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { |
continue |
} |
if c != ';' { |
i-- |
} |
break |
} |
|
entityName := string(s[1:i]) |
if entityName == "" { |
// No-op. |
} else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' { |
// No-op. |
} else if x := entity[entityName]; x != 0 { |
return dst + utf8.EncodeRune(b[dst:], x), src + i |
} else if x := entity2[entityName]; x[0] != 0 { |
dst1 := dst + utf8.EncodeRune(b[dst:], x[0]) |
return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i |
} else if !attribute { |
maxLen := len(entityName) - 1 |
if maxLen > longestEntityWithoutSemicolon { |
maxLen = longestEntityWithoutSemicolon |
} |
for j := maxLen; j > 1; j-- { |
if x := entity[entityName[:j]]; x != 0 { |
return dst + utf8.EncodeRune(b[dst:], x), src + j + 1 |
} |
} |
} |
|
dst1, src1 = dst+i, src+i |
copy(b[dst:dst1], b[src:src1]) |
return dst1, src1 |
} |
|
// unescape unescapes b's entities in-place, so that "a<b" becomes "a<b". |
func unescape(b []byte) []byte { |
for i, c := range b { |
if c == '&' { |
dst, src := unescapeEntity(b, i, i, false) |
for src < len(b) { |
c := b[src] |
if c == '&' { |
dst, src = unescapeEntity(b, dst, src, false) |
} else { |
b[dst] = c |
dst, src = dst+1, src+1 |
} |
} |
return b[0:dst] |
} |
} |
return b |
} |
|
// lower lower-cases the A-Z bytes in b in-place, so that "aBc" becomes "abc". |
func lower(b []byte) []byte { |
for i, c := range b { |
if 'A' <= c && c <= 'Z' { |
b[i] = c + 'a' - 'A' |
} |
} |
return b |
} |
|
const escapedChars = `&'<>"` |
|
func escape(w writer, s string) error { |
i := strings.IndexAny(s, escapedChars) |
for i != -1 { |
if _, err := w.WriteString(s[:i]); err != nil { |
return err |
} |
var esc string |
switch s[i] { |
case '&': |
esc = "&" |
case '\'': |
esc = "'" |
case '<': |
esc = "<" |
case '>': |
esc = ">" |
case '"': |
esc = """ |
default: |
panic("unrecognized escape character") |
} |
s = s[i+1:] |
if _, err := w.WriteString(esc); err != nil { |
return err |
} |
i = strings.IndexAny(s, escapedChars) |
} |
_, err := w.WriteString(s) |
return err |
} |
|
// EscapeString escapes special characters like "<" to become "<". It |
// escapes only five such characters: amp, apos, lt, gt and quot. |
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't |
// always true. |
func EscapeString(s string) string { |
if strings.IndexAny(s, escapedChars) == -1 { |
return s |
} |
var buf bytes.Buffer |
escape(&buf, s) |
return buf.String() |
} |
|
// UnescapeString unescapes entities like "<" to become "<". It unescapes a |
// larger range of entities than EscapeString escapes. For example, "á" |
// unescapes to "á", as does "á" and "&xE1;". |
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't |
// always true. |
func UnescapeString(s string) string { |
for _, c := range s { |
if c == '&' { |
return string(unescape([]byte(s))) |
} |
} |
return s |
} |
/template/template.go
0,0 → 1,280
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"fmt" |
"io" |
"io/ioutil" |
"path/filepath" |
"sync" |
"text/template" |
"text/template/parse" |
) |
|
// Template is a specialized template.Template that produces a safe HTML |
// document fragment. |
type Template struct { |
escaped bool |
// We could embed the text/template field, but it's safer not to because |
// we need to keep our version of the name space and the underlying |
// template's in sync. |
text *template.Template |
*nameSpace // common to all associated templates |
} |
|
// nameSpace is the data structure shared by all templates in an association. |
type nameSpace struct { |
mu sync.Mutex |
set map[string]*Template |
} |
|
// Execute applies a parsed template to the specified data object, |
// writing the output to wr. |
func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { |
t.nameSpace.mu.Lock() |
if !t.escaped { |
if err = escapeTemplates(t, t.Name()); err != nil { |
t.escaped = true |
} |
} |
t.nameSpace.mu.Unlock() |
if err != nil { |
return |
} |
return t.text.Execute(wr, data) |
} |
|
// ExecuteTemplate applies the template associated with t that has the given |
// name to the specified data object and writes the output to wr. |
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { |
tmpl, err := t.lookupAndEscapeTemplate(wr, name) |
if err != nil { |
return err |
} |
return tmpl.text.Execute(wr, data) |
} |
|
// lookupAndEscapeTemplate guarantees that the template with the given name |
// is escaped, or returns an error if it cannot be. It returns the named |
// template. |
func (t *Template) lookupAndEscapeTemplate(wr io.Writer, name string) (tmpl *Template, err error) { |
t.nameSpace.mu.Lock() |
defer t.nameSpace.mu.Unlock() |
tmpl = t.set[name] |
if (tmpl == nil) != (t.text.Lookup(name) == nil) { |
panic("html/template internal error: template escaping out of sync") |
} |
if tmpl != nil && !tmpl.escaped { |
err = escapeTemplates(tmpl, name) |
} |
return tmpl, err |
} |
|
// Parse parses a string into a template. Nested template definitions |
// will be associated with the top-level template t. Parse may be |
// called multiple times to parse definitions of templates to associate |
// with t. It is an error if a resulting template is non-empty (contains |
// content other than template definitions) and would replace a |
// non-empty template with the same name. (In multiple calls to Parse |
// with the same receiver template, only one call can contain text |
// other than space, comments, and template definitions.) |
func (t *Template) Parse(src string) (*Template, error) { |
t.nameSpace.mu.Lock() |
t.escaped = false |
t.nameSpace.mu.Unlock() |
ret, err := t.text.Parse(src) |
if err != nil { |
return nil, err |
} |
// In general, all the named templates might have changed underfoot. |
// Regardless, some new ones may have been defined. |
// The template.Template set has been updated; update ours. |
t.nameSpace.mu.Lock() |
defer t.nameSpace.mu.Unlock() |
for _, v := range ret.Templates() { |
name := v.Name() |
tmpl := t.set[name] |
if tmpl == nil { |
tmpl = t.new(name) |
} |
tmpl.escaped = false |
tmpl.text = v |
} |
return t, nil |
} |
|
// AddParseTree is unimplemented. |
func (t *Template) AddParseTree(name string, tree *parse.Tree) error { |
return fmt.Errorf("html/template: AddParseTree unimplemented") |
} |
|
// Clone is unimplemented. |
func (t *Template) Clone(name string) error { |
return fmt.Errorf("html/template: Clone unimplemented") |
} |
|
// New allocates a new HTML template with the given name. |
func New(name string) *Template { |
tmpl := &Template{ |
false, |
template.New(name), |
&nameSpace{ |
set: make(map[string]*Template), |
}, |
} |
tmpl.set[name] = tmpl |
return tmpl |
} |
|
// New allocates a new HTML template associated with the given one |
// and with the same delimiters. The association, which is transitive, |
// allows one template to invoke another with a {{template}} action. |
func (t *Template) New(name string) *Template { |
t.nameSpace.mu.Lock() |
defer t.nameSpace.mu.Unlock() |
return t.new(name) |
} |
|
// new is the implementation of New, without the lock. |
func (t *Template) new(name string) *Template { |
tmpl := &Template{ |
false, |
t.text.New(name), |
t.nameSpace, |
} |
tmpl.set[name] = tmpl |
return tmpl |
} |
|
// Name returns the name of the template. |
func (t *Template) Name() string { |
return t.text.Name() |
} |
|
// FuncMap is the type of the map defining the mapping from names to |
// functions. Each function must have either a single return value, or two |
// return values of which the second has type error. In that case, if the |
// second (error) argument evaluates to non-nil during execution, execution |
// terminates and Execute returns that error. FuncMap has the same base type |
// as template.FuncMap, copied here so clients need not import "text/template". |
type FuncMap map[string]interface{} |
|
// Funcs adds the elements of the argument map to the template's function map. |
// It panics if a value in the map is not a function with appropriate return |
// type. However, it is legal to overwrite elements of the map. The return |
// value is the template, so calls can be chained. |
func (t *Template) Funcs(funcMap FuncMap) *Template { |
t.text.Funcs(template.FuncMap(funcMap)) |
return t |
} |
|
// Delims sets the action delimiters to the specified strings, to be used in |
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template |
// definitions will inherit the settings. An empty delimiter stands for the |
// corresponding default: {{ or }}. |
// The return value is the template, so calls can be chained. |
func (t *Template) Delims(left, right string) *Template { |
t.text.Delims(left, right) |
return t |
} |
|
// Lookup returns the template with the given name that is associated with t, |
// or nil if there is no such template. |
func (t *Template) Lookup(name string) *Template { |
t.nameSpace.mu.Lock() |
defer t.nameSpace.mu.Unlock() |
return t.set[name] |
} |
|
// Must panics if err is non-nil in the same way as template.Must. |
func Must(t *Template, err error) *Template { |
if err != nil { |
panic(err) |
} |
return t |
} |
|
// ParseFiles creates a new Template and parses the template definitions from |
// the named files. The returned template's name will have the (base) name and |
// (parsed) contents of the first file. There must be at least one file. |
// If an error occurs, parsing stops and the returned *Template is nil. |
func ParseFiles(filenames ...string) (*Template, error) { |
return parseFiles(nil, filenames...) |
} |
|
// ParseFiles parses the named files and associates the resulting templates with |
// t. If an error occurs, parsing stops and the returned template is nil; |
// otherwise it is t. There must be at least one file. |
func (t *Template) ParseFiles(filenames ...string) (*Template, error) { |
return parseFiles(t, filenames...) |
} |
|
// parseFiles is the helper for the method and function. If the argument |
// template is nil, it is created from the first file. |
func parseFiles(t *Template, filenames ...string) (*Template, error) { |
if len(filenames) == 0 { |
// Not really a problem, but be consistent. |
return nil, fmt.Errorf("template: no files named in call to ParseFiles") |
} |
for _, filename := range filenames { |
b, err := ioutil.ReadFile(filename) |
if err != nil { |
return nil, err |
} |
s := string(b) |
name := filepath.Base(filename) |
// First template becomes return value if not already defined, |
// and we use that one for subsequent New calls to associate |
// all the templates together. Also, if this file has the same name |
// as t, this file becomes the contents of t, so |
// t, err := New(name).Funcs(xxx).ParseFiles(name) |
// works. Otherwise we create a new template associated with t. |
var tmpl *Template |
if t == nil { |
t = New(name) |
} |
if name == t.Name() { |
tmpl = t |
} else { |
tmpl = t.New(name) |
} |
_, err = tmpl.Parse(s) |
if err != nil { |
return nil, err |
} |
} |
return t, nil |
} |
|
// ParseGlob creates a new Template and parses the template definitions from the |
// files identified by the pattern, which must match at least one file. The |
// returned template will have the (base) name and (parsed) contents of the |
// first file matched by the pattern. ParseGlob is equivalent to calling |
// ParseFiles with the list of files matched by the pattern. |
func ParseGlob(pattern string) (*Template, error) { |
return parseGlob(nil, pattern) |
} |
|
// ParseGlob parses the template definitions in the files identified by the |
// pattern and associates the resulting templates with t. The pattern is |
// processed by filepath.Glob and must match at least one file. ParseGlob is |
// equivalent to calling t.ParseFiles with the list of files matched by the |
// pattern. |
func (t *Template) ParseGlob(pattern string) (*Template, error) { |
return parseGlob(t, pattern) |
} |
|
// parseGlob is the implementation of the function and method ParseGlob. |
func parseGlob(t *Template, pattern string) (*Template, error) { |
filenames, err := filepath.Glob(pattern) |
if err != nil { |
return nil, err |
} |
if len(filenames) == 0 { |
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) |
} |
return parseFiles(t, filenames...) |
} |
/template/js_test.go
0,0 → 1,401
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"math" |
"strings" |
"testing" |
) |
|
func TestNextJsCtx(t *testing.T) { |
tests := []struct { |
jsCtx jsCtx |
s string |
}{ |
// Statement terminators precede regexps. |
{jsCtxRegexp, ";"}, |
// This is not airtight. |
// ({ valueOf: function () { return 1 } } / 2) |
// is valid JavaScript but in practice, devs do not do this. |
// A block followed by a statement starting with a RegExp is |
// much more common: |
// while (x) {...} /foo/.test(x) || panic() |
{jsCtxRegexp, "}"}, |
// But member, call, grouping, and array expression terminators |
// precede div ops. |
{jsCtxDivOp, ")"}, |
{jsCtxDivOp, "]"}, |
// At the start of a primary expression, array, or expression |
// statement, expect a regexp. |
{jsCtxRegexp, "("}, |
{jsCtxRegexp, "["}, |
{jsCtxRegexp, "{"}, |
// Assignment operators precede regexps as do all exclusively |
// prefix and binary operators. |
{jsCtxRegexp, "="}, |
{jsCtxRegexp, "+="}, |
{jsCtxRegexp, "*="}, |
{jsCtxRegexp, "*"}, |
{jsCtxRegexp, "!"}, |
// Whether the + or - is infix or prefix, it cannot precede a |
// div op. |
{jsCtxRegexp, "+"}, |
{jsCtxRegexp, "-"}, |
// An incr/decr op precedes a div operator. |
// This is not airtight. In (g = ++/h/i) a regexp follows a |
// pre-increment operator, but in practice devs do not try to |
// increment or decrement regular expressions. |
// (g++/h/i) where ++ is a postfix operator on g is much more |
// common. |
{jsCtxDivOp, "--"}, |
{jsCtxDivOp, "++"}, |
{jsCtxDivOp, "x--"}, |
// When we have many dashes or pluses, then they are grouped |
// left to right. |
{jsCtxRegexp, "x---"}, // A postfix -- then a -. |
// return followed by a slash returns the regexp literal or the |
// slash starts a regexp literal in an expression statement that |
// is dead code. |
{jsCtxRegexp, "return"}, |
{jsCtxRegexp, "return "}, |
{jsCtxRegexp, "return\t"}, |
{jsCtxRegexp, "return\n"}, |
{jsCtxRegexp, "return\u2028"}, |
// Identifiers can be divided and cannot validly be preceded by |
// a regular expressions. Semicolon insertion cannot happen |
// between an identifier and a regular expression on a new line |
// because the one token lookahead for semicolon insertion has |
// to conclude that it could be a div binary op and treat it as |
// such. |
{jsCtxDivOp, "x"}, |
{jsCtxDivOp, "x "}, |
{jsCtxDivOp, "x\t"}, |
{jsCtxDivOp, "x\n"}, |
{jsCtxDivOp, "x\u2028"}, |
{jsCtxDivOp, "preturn"}, |
// Numbers precede div ops. |
{jsCtxDivOp, "0"}, |
// Dots that are part of a number are div preceders. |
{jsCtxDivOp, "0."}, |
} |
|
for _, test := range tests { |
if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx { |
t.Errorf("want %s got %q", test.jsCtx, test.s) |
} |
if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx { |
t.Errorf("want %s got %q", test.jsCtx, test.s) |
} |
} |
|
if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp { |
t.Error("Blank tokens") |
} |
|
if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp { |
t.Error("Blank tokens") |
} |
} |
|
func TestJSValEscaper(t *testing.T) { |
tests := []struct { |
x interface{} |
js string |
}{ |
{int(42), " 42 "}, |
{uint(42), " 42 "}, |
{int16(42), " 42 "}, |
{uint16(42), " 42 "}, |
{int32(-42), " -42 "}, |
{uint32(42), " 42 "}, |
{int16(-42), " -42 "}, |
{uint16(42), " 42 "}, |
{int64(-42), " -42 "}, |
{uint64(42), " 42 "}, |
{uint64(1) << 53, " 9007199254740992 "}, |
// ulp(1 << 53) > 1 so this loses precision in JS |
// but it is still a representable integer literal. |
{uint64(1)<<53 + 1, " 9007199254740993 "}, |
{float32(1.0), " 1 "}, |
{float32(-1.0), " -1 "}, |
{float32(0.5), " 0.5 "}, |
{float32(-0.5), " -0.5 "}, |
{float32(1.0) / float32(256), " 0.00390625 "}, |
{float32(0), " 0 "}, |
{math.Copysign(0, -1), " -0 "}, |
{float64(1.0), " 1 "}, |
{float64(-1.0), " -1 "}, |
{float64(0.5), " 0.5 "}, |
{float64(-0.5), " -0.5 "}, |
{float64(0), " 0 "}, |
{math.Copysign(0, -1), " -0 "}, |
{"", `""`}, |
{"foo", `"foo"`}, |
// Newlines. |
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`}, |
// "\v" == "v" on IE 6 so use "\x0b" instead. |
{"\t\x0b", `"\u0009\u000b"`}, |
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`}, |
{[]interface{}{}, "[]"}, |
{[]interface{}{42, "foo", nil}, `[42,"foo",null]`}, |
{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`}, |
{"<!--", `"\u003c!--"`}, |
{"-->", `"--\u003e"`}, |
{"<![CDATA[", `"\u003c![CDATA["`}, |
{"]]>", `"]]\u003e"`}, |
{"</script", `"\u003c/script"`}, |
{"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E" |
} |
|
for _, test := range tests { |
if js := jsValEscaper(test.x); js != test.js { |
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js) |
} |
// Make sure that escaping corner cases are not broken |
// by nesting. |
a := []interface{}{test.x} |
want := "[" + strings.TrimSpace(test.js) + "]" |
if js := jsValEscaper(a); js != want { |
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js) |
} |
} |
} |
|
func TestJSStrEscaper(t *testing.T) { |
tests := []struct { |
x interface{} |
esc string |
}{ |
{"", ``}, |
{"foo", `foo`}, |
{"\u0000", `\0`}, |
{"\t", `\t`}, |
{"\n", `\n`}, |
{"\r", `\r`}, |
{"\u2028", `\u2028`}, |
{"\u2029", `\u2029`}, |
{"\\", `\\`}, |
{"\\n", `\\n`}, |
{"foo\r\nbar", `foo\r\nbar`}, |
// Preserve attribute boundaries. |
{`"`, `\x22`}, |
{`'`, `\x27`}, |
// Allow embedding in HTML without further escaping. |
{`&`, `\x26amp;`}, |
// Prevent breaking out of text node and element boundaries. |
{"</script>", `\x3c\/script\x3e`}, |
{"<![CDATA[", `\x3c![CDATA[`}, |
{"]]>", `]]\x3e`}, |
// http://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span |
// "The text in style, script, title, and textarea elements |
// must not have an escaping text span start that is not |
// followed by an escaping text span end." |
// Furthermore, spoofing an escaping text span end could lead |
// to different interpretation of a </script> sequence otherwise |
// masked by the escaping text span, and spoofing a start could |
// allow regular text content to be interpreted as script |
// allowing script execution via a combination of a JS string |
// injection followed by an HTML text injection. |
{"<!--", `\x3c!--`}, |
{"-->", `--\x3e`}, |
// From http://code.google.com/p/doctype/wiki/ArticleUtf7 |
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-", |
`\x2bADw-script\x2bAD4-alert(1)\x2bADw-\/script\x2bAD4-`, |
}, |
// Invalid UTF-8 sequence |
{"foo\xA0bar", "foo\xA0bar"}, |
// Invalid unicode scalar value. |
{"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"}, |
} |
|
for _, test := range tests { |
esc := jsStrEscaper(test.x) |
if esc != test.esc { |
t.Errorf("%q: want %q got %q", test.x, test.esc, esc) |
} |
} |
} |
|
func TestJSRegexpEscaper(t *testing.T) { |
tests := []struct { |
x interface{} |
esc string |
}{ |
{"", `(?:)`}, |
{"foo", `foo`}, |
{"\u0000", `\0`}, |
{"\t", `\t`}, |
{"\n", `\n`}, |
{"\r", `\r`}, |
{"\u2028", `\u2028`}, |
{"\u2029", `\u2029`}, |
{"\\", `\\`}, |
{"\\n", `\\n`}, |
{"foo\r\nbar", `foo\r\nbar`}, |
// Preserve attribute boundaries. |
{`"`, `\x22`}, |
{`'`, `\x27`}, |
// Allow embedding in HTML without further escaping. |
{`&`, `\x26amp;`}, |
// Prevent breaking out of text node and element boundaries. |
{"</script>", `\x3c\/script\x3e`}, |
{"<![CDATA[", `\x3c!\[CDATA\[`}, |
{"]]>", `\]\]\x3e`}, |
// Escaping text spans. |
{"<!--", `\x3c!\-\-`}, |
{"-->", `\-\-\x3e`}, |
{"*", `\*`}, |
{"+", `\x2b`}, |
{"?", `\?`}, |
{"[](){}", `\[\]\(\)\{\}`}, |
{"$foo|x.y", `\$foo\|x\.y`}, |
{"x^y", `x\^y`}, |
} |
|
for _, test := range tests { |
esc := jsRegexpEscaper(test.x) |
if esc != test.esc { |
t.Errorf("%q: want %q got %q", test.x, test.esc, esc) |
} |
} |
} |
|
func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { |
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") |
|
tests := []struct { |
name string |
escaper func(...interface{}) string |
escaped string |
}{ |
{ |
"jsStrEscaper", |
jsStrEscaper, |
"\\0\x01\x02\x03\x04\x05\x06\x07" + |
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" + |
"\x10\x11\x12\x13\x14\x15\x16\x17" + |
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !\x22#$%\x26\x27()*\x2b,-.\/` + |
`0123456789:;\x3c=\x3e?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", |
}, |
{ |
"jsRegexpEscaper", |
jsRegexpEscaper, |
"\\0\x01\x02\x03\x04\x05\x06\x07" + |
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" + |
"\x10\x11\x12\x13\x14\x15\x16\x17" + |
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` + |
`0123456789:;\x3c=\x3e\?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ\[\\\]\^_` + |
"`abcdefghijklmno" + |
`pqrstuvwxyz\{\|\}~` + "\u007f" + |
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", |
}, |
} |
|
for _, test := range tests { |
if s := test.escaper(input); s != test.escaped { |
t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) |
continue |
} |
|
// Escape it rune by rune to make sure that any |
// fast-path checking does not break escaping. |
var buf bytes.Buffer |
for _, c := range input { |
buf.WriteString(test.escaper(string(c))) |
} |
|
if s := buf.String(); s != test.escaped { |
t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) |
continue |
} |
} |
} |
|
func BenchmarkJSValEscaperWithNum(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsValEscaper(3.141592654) |
} |
} |
|
func BenchmarkJSValEscaperWithStr(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
|
func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsValEscaper("The quick, brown fox jumps over the lazy dog") |
} |
} |
|
func BenchmarkJSValEscaperWithObj(b *testing.B) { |
o := struct { |
S string |
N int |
}{ |
"The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028", |
42, |
} |
for i := 0; i < b.N; i++ { |
jsValEscaper(o) |
} |
} |
|
func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) { |
o := struct { |
S string |
N int |
}{ |
"The quick, brown fox jumps over the lazy dog", |
42, |
} |
for i := 0; i < b.N; i++ { |
jsValEscaper(o) |
} |
} |
|
func BenchmarkJSStrEscaperNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsStrEscaper("The quick, brown fox jumps over the lazy dog.") |
} |
} |
|
func BenchmarkJSStrEscaper(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
|
func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsRegexpEscaper("The quick, brown fox jumps over the lazy dog") |
} |
} |
|
func BenchmarkJSRegexpEscaper(b *testing.B) { |
for i := 0; i < b.N; i++ { |
jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
/template/js.go
0,0 → 1,362
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"encoding/json" |
"fmt" |
"reflect" |
"strings" |
"unicode/utf8" |
) |
|
// nextJSCtx returns the context that determines whether a slash after the |
// given run of tokens tokens starts a regular expression instead of a division |
// operator: / or /=. |
// |
// This assumes that the token run does not include any string tokens, comment |
// tokens, regular expression literal tokens, or division operators. |
// |
// This fails on some valid but nonsensical JavaScript programs like |
// "x = ++/foo/i" which is quite different than "x++/foo/i", but is not known to |
// fail on any known useful programs. It is based on the draft |
// JavaScript 2.0 lexical grammar and requires one token of lookbehind: |
// http://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html |
func nextJSCtx(s []byte, preceding jsCtx) jsCtx { |
s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029") |
if len(s) == 0 { |
return preceding |
} |
|
// All cases below are in the single-byte UTF-8 group. |
switch c, n := s[len(s)-1], len(s); c { |
case '+', '-': |
// ++ and -- are not regexp preceders, but + and - are whether |
// they are used as infix or prefix operators. |
start := n - 1 |
// Count the number of adjacent dashes or pluses. |
for start > 0 && s[start-1] == c { |
start-- |
} |
if (n-start)&1 == 1 { |
// Reached for trailing minus signs since "---" is the |
// same as "-- -". |
return jsCtxRegexp |
} |
return jsCtxDivOp |
case '.': |
// Handle "42." |
if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' { |
return jsCtxDivOp |
} |
return jsCtxRegexp |
// Suffixes for all punctuators from section 7.7 of the language spec |
// that only end binary operators not handled above. |
case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?': |
return jsCtxRegexp |
// Suffixes for all punctuators from section 7.7 of the language spec |
// that are prefix operators not handled above. |
case '!', '~': |
return jsCtxRegexp |
// Matches all the punctuators from section 7.7 of the language spec |
// that are open brackets not handled above. |
case '(', '[': |
return jsCtxRegexp |
// Matches all the punctuators from section 7.7 of the language spec |
// that precede expression starts. |
case ':', ';', '{': |
return jsCtxRegexp |
// CAVEAT: the close punctuators ('}', ']', ')') precede div ops and |
// are handled in the default except for '}' which can precede a |
// division op as in |
// ({ valueOf: function () { return 42 } } / 2 |
// which is valid, but, in practice, developers don't divide object |
// literals, so our heuristic works well for code like |
// function () { ... } /foo/.test(x) && sideEffect(); |
// The ')' punctuator can precede a regular expression as in |
// if (b) /foo/.test(x) && ... |
// but this is much less likely than |
// (a + b) / c |
case '}': |
return jsCtxRegexp |
default: |
// Look for an IdentifierName and see if it is a keyword that |
// can precede a regular expression. |
j := n |
for j > 0 && isJSIdentPart(rune(s[j-1])) { |
j-- |
} |
if regexpPrecederKeywords[string(s[j:])] { |
return jsCtxRegexp |
} |
} |
// Otherwise is a punctuator not listed above, or |
// a string which precedes a div op, or an identifier |
// which precedes a div op. |
return jsCtxDivOp |
} |
|
// regexPrecederKeywords is a set of reserved JS keywords that can precede a |
// regular expression in JS source. |
var regexpPrecederKeywords = map[string]bool{ |
"break": true, |
"case": true, |
"continue": true, |
"delete": true, |
"do": true, |
"else": true, |
"finally": true, |
"in": true, |
"instanceof": true, |
"return": true, |
"throw": true, |
"try": true, |
"typeof": true, |
"void": true, |
} |
|
var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() |
|
// indirectToJSONMarshaler returns the value, after dereferencing as many times |
// as necessary to reach the base type (or nil) or an implementation of json.Marshal. |
func indirectToJSONMarshaler(a interface{}) interface{} { |
v := reflect.ValueOf(a) |
for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() { |
v = v.Elem() |
} |
return v.Interface() |
} |
|
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has |
// neither side-effects nor free variables outside (NaN, Infinity). |
func jsValEscaper(args ...interface{}) string { |
var a interface{} |
if len(args) == 1 { |
a = indirectToJSONMarshaler(args[0]) |
switch t := a.(type) { |
case JS: |
return string(t) |
case JSStr: |
// TODO: normalize quotes. |
return `"` + string(t) + `"` |
case json.Marshaler: |
// Do not treat as a Stringer. |
case fmt.Stringer: |
a = t.String() |
} |
} else { |
for i, arg := range args { |
args[i] = indirectToJSONMarshaler(arg) |
} |
a = fmt.Sprint(args...) |
} |
// TODO: detect cycles before calling Marshal which loops infinitely on |
// cyclic data. This may be an unacceptable DoS risk. |
|
b, err := json.Marshal(a) |
if err != nil { |
// Put a space before comment so that if it is flush against |
// a division operator it is not turned into a line comment: |
// x/{{y}} |
// turning into |
// x//* error marshalling y: |
// second line of error message */null |
return fmt.Sprintf(" /* %s */null ", strings.Replace(err.Error(), "*/", "* /", -1)) |
} |
|
// TODO: maybe post-process output to prevent it from containing |
// "<!--", "-->", "<![CDATA[", "]]>", or "</script" |
// in case custom marshallers produce output containing those. |
|
// TODO: Maybe abbreviate \u00ab to \xab to produce more compact output. |
if len(b) == 0 { |
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should |
// not cause the output `x=y/*z`. |
return " null " |
} |
first, _ := utf8.DecodeRune(b) |
last, _ := utf8.DecodeLastRune(b) |
var buf bytes.Buffer |
// Prevent IdentifierNames and NumericLiterals from running into |
// keywords: in, instanceof, typeof, void |
pad := isJSIdentPart(first) || isJSIdentPart(last) |
if pad { |
buf.WriteByte(' ') |
} |
written := 0 |
// Make sure that json.Marshal escapes codepoints U+2028 & U+2029 |
// so it falls within the subset of JSON which is valid JS. |
for i := 0; i < len(b); { |
rune, n := utf8.DecodeRune(b[i:]) |
repl := "" |
if rune == 0x2028 { |
repl = `\u2028` |
} else if rune == 0x2029 { |
repl = `\u2029` |
} |
if repl != "" { |
buf.Write(b[written:i]) |
buf.WriteString(repl) |
written = i + n |
} |
i += n |
} |
if buf.Len() != 0 { |
buf.Write(b[written:]) |
if pad { |
buf.WriteByte(' ') |
} |
b = buf.Bytes() |
} |
return string(b) |
} |
|
// jsStrEscaper produces a string that can be included between quotes in |
// JavaScript source, in JavaScript embedded in an HTML5 <script> element, |
// or in an HTML5 event handler attribute such as onclick. |
func jsStrEscaper(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeJSStr { |
return replace(s, jsStrNormReplacementTable) |
} |
return replace(s, jsStrReplacementTable) |
} |
|
// jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression |
// specials so the result is treated literally when included in a regular |
// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by |
// the literal text of {{.X}} followed by the string "bar". |
func jsRegexpEscaper(args ...interface{}) string { |
s, _ := stringify(args...) |
s = replace(s, jsRegexpReplacementTable) |
if s == "" { |
// /{{.X}}/ should not produce a line comment when .X == "". |
return "(?:)" |
} |
return s |
} |
|
// replace replaces each rune r of s with replacementTable[r], provided that |
// r < len(replacementTable). If replacementTable[r] is the empty string then |
// no replacement is made. |
// It also replaces runes U+2028 and U+2029 with the raw strings `\u2028` and |
// `\u2029`. |
func replace(s string, replacementTable []string) string { |
var b bytes.Buffer |
written := 0 |
for i, r := range s { |
var repl string |
switch { |
case int(r) < len(replacementTable) && replacementTable[r] != "": |
repl = replacementTable[r] |
case r == '\u2028': |
repl = `\u2028` |
case r == '\u2029': |
repl = `\u2029` |
default: |
continue |
} |
b.WriteString(s[written:i]) |
b.WriteString(repl) |
written = i + utf8.RuneLen(r) |
} |
if written == 0 { |
return s |
} |
b.WriteString(s[written:]) |
return b.String() |
} |
|
var jsStrReplacementTable = []string{ |
0: `\0`, |
'\t': `\t`, |
'\n': `\n`, |
'\v': `\x0b`, // "\v" == "v" on IE 6. |
'\f': `\f`, |
'\r': `\r`, |
// Encode HTML specials as hex so the output can be embedded |
// in HTML attributes without further encoding. |
'"': `\x22`, |
'&': `\x26`, |
'\'': `\x27`, |
'+': `\x2b`, |
'/': `\/`, |
'<': `\x3c`, |
'>': `\x3e`, |
'\\': `\\`, |
} |
|
// jsStrNormReplacementTable is like jsStrReplacementTable but does not |
// overencode existing escapes since this table has no entry for `\`. |
var jsStrNormReplacementTable = []string{ |
0: `\0`, |
'\t': `\t`, |
'\n': `\n`, |
'\v': `\x0b`, // "\v" == "v" on IE 6. |
'\f': `\f`, |
'\r': `\r`, |
// Encode HTML specials as hex so the output can be embedded |
// in HTML attributes without further encoding. |
'"': `\x22`, |
'&': `\x26`, |
'\'': `\x27`, |
'+': `\x2b`, |
'/': `\/`, |
'<': `\x3c`, |
'>': `\x3e`, |
} |
|
var jsRegexpReplacementTable = []string{ |
0: `\0`, |
'\t': `\t`, |
'\n': `\n`, |
'\v': `\x0b`, // "\v" == "v" on IE 6. |
'\f': `\f`, |
'\r': `\r`, |
// Encode HTML specials as hex so the output can be embedded |
// in HTML attributes without further encoding. |
'"': `\x22`, |
'$': `\$`, |
'&': `\x26`, |
'\'': `\x27`, |
'(': `\(`, |
')': `\)`, |
'*': `\*`, |
'+': `\x2b`, |
'-': `\-`, |
'.': `\.`, |
'/': `\/`, |
'<': `\x3c`, |
'>': `\x3e`, |
'?': `\?`, |
'[': `\[`, |
'\\': `\\`, |
']': `\]`, |
'^': `\^`, |
'{': `\{`, |
'|': `\|`, |
'}': `\}`, |
} |
|
// isJSIdentPart returns whether the given rune is a JS identifier part. |
// It does not handle all the non-Latin letters, joiners, and combining marks, |
// but it does handle every codepoint that can occur in a numeric literal or |
// a keyword. |
func isJSIdentPart(r rune) bool { |
switch { |
case r == '$': |
return true |
case '0' <= r && r <= '9': |
return true |
case 'A' <= r && r <= 'Z': |
return true |
case r == '_': |
return true |
case 'a' <= r && r <= 'z': |
return true |
} |
return false |
} |
/template/context.go
0,0 → 1,339
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"fmt" |
) |
|
// context describes the state an HTML parser must be in when it reaches the |
// portion of HTML produced by evaluating a particular template node. |
// |
// The zero value of type context is the start context for a template that |
// produces an HTML fragment as defined at |
// http://www.w3.org/TR/html5/the-end.html#parsing-html-fragments |
// where the context element is null. |
type context struct { |
state state |
delim delim |
urlPart urlPart |
jsCtx jsCtx |
attr attr |
element element |
err *Error |
} |
|
func (c context) String() string { |
return fmt.Sprintf("{%v %v %v %v %v %v %v}", c.state, c.delim, c.urlPart, c.jsCtx, c.attr, c.element, c.err) |
} |
|
// eq returns whether two contexts are equal. |
func (c context) eq(d context) bool { |
return c.state == d.state && |
c.delim == d.delim && |
c.urlPart == d.urlPart && |
c.jsCtx == d.jsCtx && |
c.attr == d.attr && |
c.element == d.element && |
c.err == d.err |
} |
|
// mangle produces an identifier that includes a suffix that distinguishes it |
// from template names mangled with different contexts. |
func (c context) mangle(templateName string) string { |
// The mangled name for the default context is the input templateName. |
if c.state == stateText { |
return templateName |
} |
s := templateName + "$htmltemplate_" + c.state.String() |
if c.delim != 0 { |
s += "_" + c.delim.String() |
} |
if c.urlPart != 0 { |
s += "_" + c.urlPart.String() |
} |
if c.jsCtx != 0 { |
s += "_" + c.jsCtx.String() |
} |
if c.attr != 0 { |
s += "_" + c.attr.String() |
} |
if c.element != 0 { |
s += "_" + c.element.String() |
} |
return s |
} |
|
// state describes a high-level HTML parser state. |
// |
// It bounds the top of the element stack, and by extension the HTML insertion |
// mode, but also contains state that does not correspond to anything in the |
// HTML5 parsing algorithm because a single token production in the HTML |
// grammar may contain embedded actions in a template. For instance, the quoted |
// HTML attribute produced by |
// <div title="Hello {{.World}}"> |
// is a single token in HTML's grammar but in a template spans several nodes. |
type state uint8 |
|
const ( |
// stateText is parsed character data. An HTML parser is in |
// this state when its parse position is outside an HTML tag, |
// directive, comment, and special element body. |
stateText state = iota |
// stateTag occurs before an HTML attribute or the end of a tag. |
stateTag |
// stateAttrName occurs inside an attribute name. |
// It occurs between the ^'s in ` ^name^ = value`. |
stateAttrName |
// stateAfterName occurs after an attr name has ended but before any |
// equals sign. It occurs between the ^'s in ` name^ ^= value`. |
stateAfterName |
// stateBeforeValue occurs after the equals sign but before the value. |
// It occurs between the ^'s in ` name =^ ^value`. |
stateBeforeValue |
// stateHTMLCmt occurs inside an <!-- HTML comment -->. |
stateHTMLCmt |
// stateRCDATA occurs inside an RCDATA element (<textarea> or <title>) |
// as described at http://dev.w3.org/html5/spec/syntax.html#elements-0 |
stateRCDATA |
// stateAttr occurs inside an HTML attribute whose content is text. |
stateAttr |
// stateURL occurs inside an HTML attribute whose content is a URL. |
stateURL |
// stateJS occurs inside an event handler or script element. |
stateJS |
// stateJSDqStr occurs inside a JavaScript double quoted string. |
stateJSDqStr |
// stateJSSqStr occurs inside a JavaScript single quoted string. |
stateJSSqStr |
// stateJSRegexp occurs inside a JavaScript regexp literal. |
stateJSRegexp |
// stateJSBlockCmt occurs inside a JavaScript /* block comment */. |
stateJSBlockCmt |
// stateJSLineCmt occurs inside a JavaScript // line comment. |
stateJSLineCmt |
// stateCSS occurs inside a <style> element or style attribute. |
stateCSS |
// stateCSSDqStr occurs inside a CSS double quoted string. |
stateCSSDqStr |
// stateCSSSqStr occurs inside a CSS single quoted string. |
stateCSSSqStr |
// stateCSSDqURL occurs inside a CSS double quoted url("..."). |
stateCSSDqURL |
// stateCSSSqURL occurs inside a CSS single quoted url('...'). |
stateCSSSqURL |
// stateCSSURL occurs inside a CSS unquoted url(...). |
stateCSSURL |
// stateCSSBlockCmt occurs inside a CSS /* block comment */. |
stateCSSBlockCmt |
// stateCSSLineCmt occurs inside a CSS // line comment. |
stateCSSLineCmt |
// stateError is an infectious error state outside any valid |
// HTML/CSS/JS construct. |
stateError |
) |
|
var stateNames = [...]string{ |
stateText: "stateText", |
stateTag: "stateTag", |
stateAttrName: "stateAttrName", |
stateAfterName: "stateAfterName", |
stateBeforeValue: "stateBeforeValue", |
stateHTMLCmt: "stateHTMLCmt", |
stateRCDATA: "stateRCDATA", |
stateAttr: "stateAttr", |
stateURL: "stateURL", |
stateJS: "stateJS", |
stateJSDqStr: "stateJSDqStr", |
stateJSSqStr: "stateJSSqStr", |
stateJSRegexp: "stateJSRegexp", |
stateJSBlockCmt: "stateJSBlockCmt", |
stateJSLineCmt: "stateJSLineCmt", |
stateCSS: "stateCSS", |
stateCSSDqStr: "stateCSSDqStr", |
stateCSSSqStr: "stateCSSSqStr", |
stateCSSDqURL: "stateCSSDqURL", |
stateCSSSqURL: "stateCSSSqURL", |
stateCSSURL: "stateCSSURL", |
stateCSSBlockCmt: "stateCSSBlockCmt", |
stateCSSLineCmt: "stateCSSLineCmt", |
stateError: "stateError", |
} |
|
func (s state) String() string { |
if int(s) < len(stateNames) { |
return stateNames[s] |
} |
return fmt.Sprintf("illegal state %d", int(s)) |
} |
|
// isComment is true for any state that contains content meant for template |
// authors & maintainers, not for end-users or machines. |
func isComment(s state) bool { |
switch s { |
case stateHTMLCmt, stateJSBlockCmt, stateJSLineCmt, stateCSSBlockCmt, stateCSSLineCmt: |
return true |
} |
return false |
} |
|
// isInTag return whether s occurs solely inside an HTML tag. |
func isInTag(s state) bool { |
switch s { |
case stateTag, stateAttrName, stateAfterName, stateBeforeValue, stateAttr: |
return true |
} |
return false |
} |
|
// delim is the delimiter that will end the current HTML attribute. |
type delim uint8 |
|
const ( |
// delimNone occurs outside any attribute. |
delimNone delim = iota |
// delimDoubleQuote occurs when a double quote (") closes the attribute. |
delimDoubleQuote |
// delimSingleQuote occurs when a single quote (') closes the attribute. |
delimSingleQuote |
// delimSpaceOrTagEnd occurs when a space or right angle bracket (>) |
// closes the attribute. |
delimSpaceOrTagEnd |
) |
|
var delimNames = [...]string{ |
delimNone: "delimNone", |
delimDoubleQuote: "delimDoubleQuote", |
delimSingleQuote: "delimSingleQuote", |
delimSpaceOrTagEnd: "delimSpaceOrTagEnd", |
} |
|
func (d delim) String() string { |
if int(d) < len(delimNames) { |
return delimNames[d] |
} |
return fmt.Sprintf("illegal delim %d", int(d)) |
} |
|
// urlPart identifies a part in an RFC 3986 hierarchical URL to allow different |
// encoding strategies. |
type urlPart uint8 |
|
const ( |
// urlPartNone occurs when not in a URL, or possibly at the start: |
// ^ in "^http://auth/path?k=v#frag". |
urlPartNone urlPart = iota |
// urlPartPreQuery occurs in the scheme, authority, or path; between the |
// ^s in "h^ttp://auth/path^?k=v#frag". |
urlPartPreQuery |
// urlPartQueryOrFrag occurs in the query portion between the ^s in |
// "http://auth/path?^k=v#frag^". |
urlPartQueryOrFrag |
// urlPartUnknown occurs due to joining of contexts both before and |
// after the query separator. |
urlPartUnknown |
) |
|
var urlPartNames = [...]string{ |
urlPartNone: "urlPartNone", |
urlPartPreQuery: "urlPartPreQuery", |
urlPartQueryOrFrag: "urlPartQueryOrFrag", |
urlPartUnknown: "urlPartUnknown", |
} |
|
func (u urlPart) String() string { |
if int(u) < len(urlPartNames) { |
return urlPartNames[u] |
} |
return fmt.Sprintf("illegal urlPart %d", int(u)) |
} |
|
// jsCtx determines whether a '/' starts a regular expression literal or a |
// division operator. |
type jsCtx uint8 |
|
const ( |
// jsCtxRegexp occurs where a '/' would start a regexp literal. |
jsCtxRegexp jsCtx = iota |
// jsCtxDivOp occurs where a '/' would start a division operator. |
jsCtxDivOp |
// jsCtxUnknown occurs where a '/' is ambiguous due to context joining. |
jsCtxUnknown |
) |
|
func (c jsCtx) String() string { |
switch c { |
case jsCtxRegexp: |
return "jsCtxRegexp" |
case jsCtxDivOp: |
return "jsCtxDivOp" |
case jsCtxUnknown: |
return "jsCtxUnknown" |
} |
return fmt.Sprintf("illegal jsCtx %d", int(c)) |
} |
|
// element identifies the HTML element when inside a start tag or special body. |
// Certain HTML element (for example <script> and <style>) have bodies that are |
// treated differently from stateText so the element type is necessary to |
// transition into the correct context at the end of a tag and to identify the |
// end delimiter for the body. |
type element uint8 |
|
const ( |
// elementNone occurs outside a special tag or special element body. |
elementNone element = iota |
// elementScript corresponds to the raw text <script> element. |
elementScript |
// elementStyle corresponds to the raw text <style> element. |
elementStyle |
// elementTextarea corresponds to the RCDATA <textarea> element. |
elementTextarea |
// elementTitle corresponds to the RCDATA <title> element. |
elementTitle |
) |
|
var elementNames = [...]string{ |
elementNone: "elementNone", |
elementScript: "elementScript", |
elementStyle: "elementStyle", |
elementTextarea: "elementTextarea", |
elementTitle: "elementTitle", |
} |
|
func (e element) String() string { |
if int(e) < len(elementNames) { |
return elementNames[e] |
} |
return fmt.Sprintf("illegal element %d", int(e)) |
} |
|
// attr identifies the most recent HTML attribute when inside a start tag. |
type attr uint8 |
|
const ( |
// attrNone corresponds to a normal attribute or no attribute. |
attrNone attr = iota |
// attrScript corresponds to an event handler attribute. |
attrScript |
// attrStyle corresponds to the style attribute whose value is CSS. |
attrStyle |
// attrURL corresponds to an attribute whose value is a URL. |
attrURL |
) |
|
var attrNames = [...]string{ |
attrNone: "attrNone", |
attrScript: "attrScript", |
attrStyle: "attrStyle", |
attrURL: "attrURL", |
} |
|
func (a attr) String() string { |
if int(a) < len(attrNames) { |
return attrNames[a] |
} |
return fmt.Sprintf("illegal attr %d", int(a)) |
} |
/template/css_test.go
0,0 → 1,281
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"strconv" |
"strings" |
"testing" |
) |
|
func TestEndsWithCSSKeyword(t *testing.T) { |
tests := []struct { |
css, kw string |
want bool |
}{ |
{"", "url", false}, |
{"url", "url", true}, |
{"URL", "url", true}, |
{"Url", "url", true}, |
{"url", "important", false}, |
{"important", "important", true}, |
{"image-url", "url", false}, |
{"imageurl", "url", false}, |
{"image url", "url", true}, |
} |
for _, test := range tests { |
got := endsWithCSSKeyword([]byte(test.css), test.kw) |
if got != test.want { |
t.Errorf("want %t but got %t for css=%v, kw=%v", test.want, got, test.css, test.kw) |
} |
} |
} |
|
func TestIsCSSNmchar(t *testing.T) { |
tests := []struct { |
rune rune |
want bool |
}{ |
{0, false}, |
{'0', true}, |
{'9', true}, |
{'A', true}, |
{'Z', true}, |
{'a', true}, |
{'z', true}, |
{'_', true}, |
{'-', true}, |
{':', false}, |
{';', false}, |
{' ', false}, |
{0x7f, false}, |
{0x80, true}, |
{0x1234, true}, |
{0xd800, false}, |
{0xdc00, false}, |
{0xfffe, false}, |
{0x10000, true}, |
{0x110000, false}, |
} |
for _, test := range tests { |
got := isCSSNmchar(test.rune) |
if got != test.want { |
t.Errorf("%q: want %t but got %t", string(test.rune), test.want, got) |
} |
} |
} |
|
func TestDecodeCSS(t *testing.T) { |
tests := []struct { |
css, want string |
}{ |
{``, ``}, |
{`foo`, `foo`}, |
{`foo\`, `foo`}, |
{`foo\\`, `foo\`}, |
{`\`, ``}, |
{`\A`, "\n"}, |
{`\a`, "\n"}, |
{`\0a`, "\n"}, |
{`\00000a`, "\n"}, |
{`\000000a`, "\u0000a"}, |
{`\1234 5`, "\u1234" + "5"}, |
{`\1234\20 5`, "\u1234" + " 5"}, |
{`\1234\A 5`, "\u1234" + "\n5"}, |
{"\\1234\t5", "\u1234" + "5"}, |
{"\\1234\n5", "\u1234" + "5"}, |
{"\\1234\r\n5", "\u1234" + "5"}, |
{`\12345`, "\U00012345"}, |
{`\\`, `\`}, |
{`\\ `, `\ `}, |
{`\"`, `"`}, |
{`\'`, `'`}, |
{`\.`, `.`}, |
{`\. .`, `. .`}, |
{ |
`The \3c i\3equick\3c/i\3e,\d\A\3cspan style=\27 color:brown\27\3e brown\3c/span\3e fox jumps\2028over the \3c canine class=\22lazy\22 \3e dog\3c/canine\3e`, |
"The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>", |
}, |
} |
for _, test := range tests { |
got1 := string(decodeCSS([]byte(test.css))) |
if got1 != test.want { |
t.Errorf("%q: want\n\t%q\nbut got\n\t%q", test.css, test.want, got1) |
} |
recoded := cssEscaper(got1) |
if got2 := string(decodeCSS([]byte(recoded))); got2 != test.want { |
t.Errorf("%q: escape & decode not dual for %q", test.css, recoded) |
} |
} |
} |
|
func TestHexDecode(t *testing.T) { |
for i := 0; i < 0x200000; i += 101 /* coprime with 16 */ { |
s := strconv.FormatInt(int64(i), 16) |
if got := int(hexDecode([]byte(s))); got != i { |
t.Errorf("%s: want %d but got %d", s, i, got) |
} |
s = strings.ToUpper(s) |
if got := int(hexDecode([]byte(s))); got != i { |
t.Errorf("%s: want %d but got %d", s, i, got) |
} |
} |
} |
|
func TestSkipCSSSpace(t *testing.T) { |
tests := []struct { |
css, want string |
}{ |
{"", ""}, |
{"foo", "foo"}, |
{"\n", ""}, |
{"\r\n", ""}, |
{"\r", ""}, |
{"\t", ""}, |
{" ", ""}, |
{"\f", ""}, |
{" foo", "foo"}, |
{" foo", " foo"}, |
{`\20`, `\20`}, |
} |
for _, test := range tests { |
got := string(skipCSSSpace([]byte(test.css))) |
if got != test.want { |
t.Errorf("%q: want %q but got %q", test.css, test.want, got) |
} |
} |
} |
|
func TestCSSEscaper(t *testing.T) { |
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") |
|
want := ("\\0\x01\x02\x03\x04\x05\x06\x07" + |
"\x08\\9 \\a\x0b\\c \\d\x0E\x0F" + |
"\x10\x11\x12\x13\x14\x15\x16\x17" + |
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !\22#$%\26\27\28\29*\2b,-.\2f ` + |
`0123456789\3a\3b\3c=\3e?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\\]^_` + |
"`abcdefghijklmno" + |
`pqrstuvwxyz\7b|\7d~` + "\u007f" + |
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") |
|
got := cssEscaper(input) |
if got != want { |
t.Errorf("encode: want\n\t%q\nbut got\n\t%q", want, got) |
} |
|
got = string(decodeCSS([]byte(got))) |
if input != got { |
t.Errorf("decode: want\n\t%q\nbut got\n\t%q", input, got) |
} |
} |
|
func TestCSSValueFilter(t *testing.T) { |
tests := []struct { |
css, want string |
}{ |
{"", ""}, |
{"foo", "foo"}, |
{"0", "0"}, |
{"0px", "0px"}, |
{"-5px", "-5px"}, |
{"1.25in", "1.25in"}, |
{"+.33em", "+.33em"}, |
{"100%", "100%"}, |
{"12.5%", "12.5%"}, |
{".foo", ".foo"}, |
{"#bar", "#bar"}, |
{"corner-radius", "corner-radius"}, |
{"-moz-corner-radius", "-moz-corner-radius"}, |
{"#000", "#000"}, |
{"#48f", "#48f"}, |
{"#123456", "#123456"}, |
{"U+00-FF, U+980-9FF", "U+00-FF, U+980-9FF"}, |
{"color: red", "color: red"}, |
{"<!--", "ZgotmplZ"}, |
{"-->", "ZgotmplZ"}, |
{"<![CDATA[", "ZgotmplZ"}, |
{"]]>", "ZgotmplZ"}, |
{"</style", "ZgotmplZ"}, |
{`"`, "ZgotmplZ"}, |
{`'`, "ZgotmplZ"}, |
{"`", "ZgotmplZ"}, |
{"\x00", "ZgotmplZ"}, |
{"/* foo */", "ZgotmplZ"}, |
{"//", "ZgotmplZ"}, |
{"[href=~", "ZgotmplZ"}, |
{"expression(alert(1337))", "ZgotmplZ"}, |
{"-expression(alert(1337))", "ZgotmplZ"}, |
{"expression", "ZgotmplZ"}, |
{"Expression", "ZgotmplZ"}, |
{"EXPRESSION", "ZgotmplZ"}, |
{"-moz-binding", "ZgotmplZ"}, |
{"-expr\x00ession(alert(1337))", "ZgotmplZ"}, |
{`-expr\0ession(alert(1337))`, "ZgotmplZ"}, |
{`-express\69on(alert(1337))`, "ZgotmplZ"}, |
{`-express\69 on(alert(1337))`, "ZgotmplZ"}, |
{`-exp\72 ession(alert(1337))`, "ZgotmplZ"}, |
{`-exp\52 ession(alert(1337))`, "ZgotmplZ"}, |
{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"}, |
{`-expre\0000073sion`, "-expre\x073sion"}, |
{`@import url evil.css`, "ZgotmplZ"}, |
} |
for _, test := range tests { |
got := cssValueFilter(test.css) |
if got != test.want { |
t.Errorf("%q: want %q but got %q", test.css, test.want, got) |
} |
} |
} |
|
func BenchmarkCSSEscaper(b *testing.B) { |
for i := 0; i < b.N; i++ { |
cssEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
|
func BenchmarkCSSEscaperNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
cssEscaper("The quick, brown fox jumps over the lazy dog.") |
} |
} |
|
func BenchmarkDecodeCSS(b *testing.B) { |
s := []byte(`The \3c i\3equick\3c/i\3e,\d\A\3cspan style=\27 color:brown\27\3e brown\3c/span\3e fox jumps\2028over the \3c canine class=\22lazy\22 \3edog\3c/canine\3e`) |
b.ResetTimer() |
for i := 0; i < b.N; i++ { |
decodeCSS(s) |
} |
} |
|
func BenchmarkDecodeCSSNoSpecials(b *testing.B) { |
s := []byte("The quick, brown fox jumps over the lazy dog.") |
b.ResetTimer() |
for i := 0; i < b.N; i++ { |
decodeCSS(s) |
} |
} |
|
func BenchmarkCSSValueFilter(b *testing.B) { |
for i := 0; i < b.N; i++ { |
cssValueFilter(` e\78preS\0Sio/**/n(alert(1337))`) |
} |
} |
|
func BenchmarkCSSValueFilterOk(b *testing.B) { |
for i := 0; i < b.N; i++ { |
cssValueFilter(`Times New Roman`) |
} |
} |
/template/css.go
0,0 → 1,268
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"fmt" |
"unicode" |
"unicode/utf8" |
) |
|
// endsWithCSSKeyword returns whether b ends with an ident that |
// case-insensitively matches the lower-case kw. |
func endsWithCSSKeyword(b []byte, kw string) bool { |
i := len(b) - len(kw) |
if i < 0 { |
// Too short. |
return false |
} |
if i != 0 { |
r, _ := utf8.DecodeLastRune(b[:i]) |
if isCSSNmchar(r) { |
// Too long. |
return false |
} |
} |
// Many CSS keywords, such as "!important" can have characters encoded, |
// but the URI production does not allow that according to |
// http://www.w3.org/TR/css3-syntax/#TOK-URI |
// This does not attempt to recognize encoded keywords. For example, |
// given "\75\72\6c" and "url" this return false. |
return string(bytes.ToLower(b[i:])) == kw |
} |
|
// isCSSNmchar returns whether rune is allowed anywhere in a CSS identifier. |
func isCSSNmchar(r rune) bool { |
// Based on the CSS3 nmchar production but ignores multi-rune escape |
// sequences. |
// http://www.w3.org/TR/css3-syntax/#SUBTOK-nmchar |
return 'a' <= r && r <= 'z' || |
'A' <= r && r <= 'Z' || |
'0' <= r && r <= '9' || |
r == '-' || |
r == '_' || |
// Non-ASCII cases below. |
0x80 <= r && r <= 0xd7ff || |
0xe000 <= r && r <= 0xfffd || |
0x10000 <= r && r <= 0x10ffff |
} |
|
// decodeCSS decodes CSS3 escapes given a sequence of stringchars. |
// If there is no change, it returns the input, otherwise it returns a slice |
// backed by a new array. |
// http://www.w3.org/TR/css3-syntax/#SUBTOK-stringchar defines stringchar. |
func decodeCSS(s []byte) []byte { |
i := bytes.IndexByte(s, '\\') |
if i == -1 { |
return s |
} |
// The UTF-8 sequence for a codepoint is never longer than 1 + the |
// number hex digits need to represent that codepoint, so len(s) is an |
// upper bound on the output length. |
b := make([]byte, 0, len(s)) |
for len(s) != 0 { |
i := bytes.IndexByte(s, '\\') |
if i == -1 { |
i = len(s) |
} |
b, s = append(b, s[:i]...), s[i:] |
if len(s) < 2 { |
break |
} |
// http://www.w3.org/TR/css3-syntax/#SUBTOK-escape |
// escape ::= unicode | '\' [#x20-#x7E#x80-#xD7FF#xE000-#xFFFD#x10000-#x10FFFF] |
if isHex(s[1]) { |
// http://www.w3.org/TR/css3-syntax/#SUBTOK-unicode |
// unicode ::= '\' [0-9a-fA-F]{1,6} wc? |
j := 2 |
for j < len(s) && j < 7 && isHex(s[j]) { |
j++ |
} |
r := hexDecode(s[1:j]) |
if r > unicode.MaxRune { |
r, j = r/16, j-1 |
} |
n := utf8.EncodeRune(b[len(b):cap(b)], r) |
// The optional space at the end allows a hex |
// sequence to be followed by a literal hex. |
// string(decodeCSS([]byte(`\A B`))) == "\nB" |
b, s = b[:len(b)+n], skipCSSSpace(s[j:]) |
} else { |
// `\\` decodes to `\` and `\"` to `"`. |
_, n := utf8.DecodeRune(s[1:]) |
b, s = append(b, s[1:1+n]...), s[1+n:] |
} |
} |
return b |
} |
|
// isHex returns whether the given character is a hex digit. |
func isHex(c byte) bool { |
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' |
} |
|
// hexDecode decodes a short hex digit sequence: "10" -> 16. |
func hexDecode(s []byte) rune { |
n := '\x00' |
for _, c := range s { |
n <<= 4 |
switch { |
case '0' <= c && c <= '9': |
n |= rune(c - '0') |
case 'a' <= c && c <= 'f': |
n |= rune(c-'a') + 10 |
case 'A' <= c && c <= 'F': |
n |= rune(c-'A') + 10 |
default: |
panic(fmt.Sprintf("Bad hex digit in %q", s)) |
} |
} |
return n |
} |
|
// skipCSSSpace returns a suffix of c, skipping over a single space. |
func skipCSSSpace(c []byte) []byte { |
if len(c) == 0 { |
return c |
} |
// wc ::= #x9 | #xA | #xC | #xD | #x20 |
switch c[0] { |
case '\t', '\n', '\f', ' ': |
return c[1:] |
case '\r': |
// This differs from CSS3's wc production because it contains a |
// probable spec error whereby wc contains all the single byte |
// sequences in nl (newline) but not CRLF. |
if len(c) >= 2 && c[1] == '\n' { |
return c[2:] |
} |
return c[1:] |
} |
return c |
} |
|
// isCSSSpace returns whether b is a CSS space char as defined in wc. |
func isCSSSpace(b byte) bool { |
switch b { |
case '\t', '\n', '\f', '\r', ' ': |
return true |
} |
return false |
} |
|
// cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes. |
func cssEscaper(args ...interface{}) string { |
s, _ := stringify(args...) |
var b bytes.Buffer |
written := 0 |
for i, r := range s { |
var repl string |
switch r { |
case 0: |
repl = `\0` |
case '\t': |
repl = `\9` |
case '\n': |
repl = `\a` |
case '\f': |
repl = `\c` |
case '\r': |
repl = `\d` |
// Encode HTML specials as hex so the output can be embedded |
// in HTML attributes without further encoding. |
case '"': |
repl = `\22` |
case '&': |
repl = `\26` |
case '\'': |
repl = `\27` |
case '(': |
repl = `\28` |
case ')': |
repl = `\29` |
case '+': |
repl = `\2b` |
case '/': |
repl = `\2f` |
case ':': |
repl = `\3a` |
case ';': |
repl = `\3b` |
case '<': |
repl = `\3c` |
case '>': |
repl = `\3e` |
case '\\': |
repl = `\\` |
case '{': |
repl = `\7b` |
case '}': |
repl = `\7d` |
default: |
continue |
} |
b.WriteString(s[written:i]) |
b.WriteString(repl) |
written = i + utf8.RuneLen(r) |
if repl != `\\` && (written == len(s) || isHex(s[written]) || isCSSSpace(s[written])) { |
b.WriteByte(' ') |
} |
} |
if written == 0 { |
return s |
} |
b.WriteString(s[written:]) |
return b.String() |
} |
|
var expressionBytes = []byte("expression") |
var mozBindingBytes = []byte("mozbinding") |
|
// cssValueFilter allows innocuous CSS values in the output including CSS |
// quantities (10px or 25%), ID or class literals (#foo, .bar), keyword values |
// (inherit, blue), and colors (#888). |
// It filters out unsafe values, such as those that affect token boundaries, |
// and anything that might execute scripts. |
func cssValueFilter(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeCSS { |
return s |
} |
b, id := decodeCSS([]byte(s)), make([]byte, 0, 64) |
|
// CSS3 error handling is specified as honoring string boundaries per |
// http://www.w3.org/TR/css3-syntax/#error-handling : |
// Malformed declarations. User agents must handle unexpected |
// tokens encountered while parsing a declaration by reading until |
// the end of the declaration, while observing the rules for |
// matching pairs of (), [], {}, "", and '', and correctly handling |
// escapes. For example, a malformed declaration may be missing a |
// property, colon (:) or value. |
// So we need to make sure that values do not have mismatched bracket |
// or quote characters to prevent the browser from restarting parsing |
// inside a string that might embed JavaScript source. |
for i, c := range b { |
switch c { |
case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}': |
return filterFailsafe |
case '-': |
// Disallow <!-- or -->. |
// -- should not appear in valid identifiers. |
if i != 0 && b[i-1] == '-' { |
return filterFailsafe |
} |
default: |
if c < 0x80 && isCSSNmchar(rune(c)) { |
id = append(id, c) |
} |
} |
} |
id = bytes.ToLower(id) |
if bytes.Index(id, expressionBytes) != -1 || bytes.Index(id, mozBindingBytes) != -1 { |
return filterFailsafe |
} |
return string(b) |
} |
/template/error.go
0,0 → 1,197
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"fmt" |
) |
|
// Error describes a problem encountered during template Escaping. |
type Error struct { |
// ErrorCode describes the kind of error. |
ErrorCode ErrorCode |
// Name is the name of the template in which the error was encountered. |
Name string |
// Line is the line number of the error in the template source or 0. |
Line int |
// Description is a human-readable description of the problem. |
Description string |
} |
|
// ErrorCode is a code for a kind of error. |
type ErrorCode int |
|
// We define codes for each error that manifests while escaping templates, but |
// escaped templates may also fail at runtime. |
// |
// Output: "ZgotmplZ" |
// Example: |
// <img src="{{.X}}"> |
// where {{.X}} evaluates to `javascript:...` |
// Discussion: |
// "ZgotmplZ" is a special value that indicates that unsafe content reached a |
// CSS or URL context at runtime. The output of the example will be |
// <img src="#ZgotmplZ"> |
// If the data comes from a trusted source, use content types to exempt it |
// from filtering: URL(`javascript:...`). |
const ( |
// OK indicates the lack of an error. |
OK ErrorCode = iota |
|
// ErrAmbigContext: "... appears in an ambiguous URL context" |
// Example: |
// <a href=" |
// {{if .C}} |
// /path/ |
// {{else}} |
// /search?q= |
// {{end}} |
// {{.X}} |
// "> |
// Discussion: |
// {{.X}} is in an ambiguous URL context since, depending on {{.C}}, |
// it may be either a URL suffix or a query parameter. |
// Moving {{.X}} into the condition removes the ambiguity: |
// <a href="{{if .C}}/path/{{.X}}{{else}}/search?q={{.X}}"> |
ErrAmbigContext |
|
// ErrBadHTML: "expected space, attr name, or end of tag, but got ...", |
// "... in unquoted attr", "... in attribute name" |
// Example: |
// <a href = /search?q=foo> |
// <href=foo> |
// <form na<e=...> |
// <option selected< |
// Discussion: |
// This is often due to a typo in an HTML element, but some runes |
// are banned in tag names, attribute names, and unquoted attribute |
// values because they can tickle parser ambiguities. |
// Quoting all attributes is the best policy. |
ErrBadHTML |
|
// ErrBranchEnd: "{{if}} branches end in different contexts" |
// Example: |
// {{if .C}}<a href="{{end}}{{.X}} |
// Discussion: |
// Package html/template statically examines each path through an |
// {{if}}, {{range}}, or {{with}} to escape any following pipelines. |
// The example is ambiguous since {{.X}} might be an HTML text node, |
// or a URL prefix in an HTML attribute. The context of {{.X}} is |
// used to figure out how to escape it, but that context depends on |
// the run-time value of {{.C}} which is not statically known. |
// |
// The problem is usually something like missing quotes or angle |
// brackets, or can be avoided by refactoring to put the two contexts |
// into different branches of an if, range or with. If the problem |
// is in a {{range}} over a collection that should never be empty, |
// adding a dummy {{else}} can help. |
ErrBranchEnd |
|
// ErrEndContext: "... ends in a non-text context: ..." |
// Examples: |
// <div |
// <div title="no close quote> |
// <script>f() |
// Discussion: |
// Executed templates should produce a DocumentFragment of HTML. |
// Templates that end without closing tags will trigger this error. |
// Templates that should not be used in an HTML context or that |
// produce incomplete Fragments should not be executed directly. |
// |
// {{define "main"}} <script>{{template "helper"}}</script> {{end}} |
// {{define "helper"}} document.write(' <div title=" ') {{end}} |
// |
// "helper" does not produce a valid document fragment, so should |
// not be Executed directly. |
ErrEndContext |
|
// ErrNoSuchTemplate: "no such template ..." |
// Examples: |
// {{define "main"}}<div {{template "attrs"}}>{{end}} |
// {{define "attrs"}}href="{{.URL}}"{{end}} |
// Discussion: |
// Package html/template looks through template calls to compute the |
// context. |
// Here the {{.URL}} in "attrs" must be treated as a URL when called |
// from "main", but you will get this error if "attrs" is not defined |
// when "main" is parsed. |
ErrNoSuchTemplate |
|
// ErrOutputContext: "cannot compute output context for template ..." |
// Examples: |
// {{define "t"}}{{if .T}}{{template "t" .T}}{{end}}{{.H}}",{{end}} |
// Discussion: |
// A recursive template does not end in the same context in which it |
// starts, and a reliable output context cannot be computed. |
// Look for typos in the named template. |
// If the template should not be called in the named start context, |
// look for calls to that template in unexpected contexts. |
// Maybe refactor recursive templates to not be recursive. |
ErrOutputContext |
|
// ErrPartialCharset: "unfinished JS regexp charset in ..." |
// Example: |
// <script>var pattern = /foo[{{.Chars}}]/</script> |
// Discussion: |
// Package html/template does not support interpolation into regular |
// expression literal character sets. |
ErrPartialCharset |
|
// ErrPartialEscape: "unfinished escape sequence in ..." |
// Example: |
// <script>alert("\{{.X}}")</script> |
// Discussion: |
// Package html/template does not support actions following a |
// backslash. |
// This is usually an error and there are better solutions; for |
// example |
// <script>alert("{{.X}}")</script> |
// should work, and if {{.X}} is a partial escape sequence such as |
// "xA0", mark the whole sequence as safe content: JSStr(`\xA0`) |
ErrPartialEscape |
|
// ErrRangeLoopReentry: "on range loop re-entry: ..." |
// Example: |
// <script>var x = [{{range .}}'{{.}},{{end}}]</script> |
// Discussion: |
// If an iteration through a range would cause it to end in a |
// different context than an earlier pass, there is no single context. |
// In the example, there is missing a quote, so it is not clear |
// whether {{.}} is meant to be inside a JS string or in a JS value |
// context. The second iteration would produce something like |
// |
// <script>var x = ['firstValue,'secondValue]</script> |
ErrRangeLoopReentry |
|
// ErrSlashAmbig: '/' could start a division or regexp. |
// Example: |
// <script> |
// {{if .C}}var x = 1{{end}} |
// /-{{.N}}/i.test(x) ? doThis : doThat(); |
// </script> |
// Discussion: |
// The example above could produce `var x = 1/-2/i.test(s)...` |
// in which the first '/' is a mathematical division operator or it |
// could produce `/-2/i.test(s)` in which the first '/' starts a |
// regexp literal. |
// Look for missing semicolons inside branches, and maybe add |
// parentheses to make it clear which interpretation you intend. |
ErrSlashAmbig |
) |
|
func (e *Error) Error() string { |
if e.Line != 0 { |
return fmt.Sprintf("html/template:%s:%d: %s", e.Name, e.Line, e.Description) |
} else if e.Name != "" { |
return fmt.Sprintf("html/template:%s: %s", e.Name, e.Description) |
} |
return "html/template: " + e.Description |
} |
|
// errorf creates an error given a format string f and args. |
// The template Name still needs to be supplied. |
func errorf(k ErrorCode, line int, f string, args ...interface{}) *Error { |
return &Error{k, "", line, fmt.Sprintf(f, args...)} |
} |
/template/transition.go
0,0 → 1,553
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"strings" |
) |
|
// transitionFunc is the array of context transition functions for text nodes. |
// A transition function takes a context and template text input, and returns |
// the updated context and the number of bytes consumed from the front of the |
// input. |
var transitionFunc = [...]func(context, []byte) (context, int){ |
stateText: tText, |
stateTag: tTag, |
stateAttrName: tAttrName, |
stateAfterName: tAfterName, |
stateBeforeValue: tBeforeValue, |
stateHTMLCmt: tHTMLCmt, |
stateRCDATA: tSpecialTagEnd, |
stateAttr: tAttr, |
stateURL: tURL, |
stateJS: tJS, |
stateJSDqStr: tJSDelimited, |
stateJSSqStr: tJSDelimited, |
stateJSRegexp: tJSDelimited, |
stateJSBlockCmt: tBlockCmt, |
stateJSLineCmt: tLineCmt, |
stateCSS: tCSS, |
stateCSSDqStr: tCSSStr, |
stateCSSSqStr: tCSSStr, |
stateCSSDqURL: tCSSStr, |
stateCSSSqURL: tCSSStr, |
stateCSSURL: tCSSStr, |
stateCSSBlockCmt: tBlockCmt, |
stateCSSLineCmt: tLineCmt, |
stateError: tError, |
} |
|
var commentStart = []byte("<!--") |
var commentEnd = []byte("-->") |
|
// tText is the context transition function for the text state. |
func tText(c context, s []byte) (context, int) { |
k := 0 |
for { |
i := k + bytes.IndexByte(s[k:], '<') |
if i < k || i+1 == len(s) { |
return c, len(s) |
} else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) { |
return context{state: stateHTMLCmt}, i + 4 |
} |
i++ |
end := false |
if s[i] == '/' { |
if i+1 == len(s) { |
return c, len(s) |
} |
end, i = true, i+1 |
} |
j, e := eatTagName(s, i) |
if j != i { |
if end { |
e = elementNone |
} |
// We've found an HTML tag. |
return context{state: stateTag, element: e}, j |
} |
k = j |
} |
panic("unreachable") |
} |
|
var elementContentType = [...]state{ |
elementNone: stateText, |
elementScript: stateJS, |
elementStyle: stateCSS, |
elementTextarea: stateRCDATA, |
elementTitle: stateRCDATA, |
} |
|
// tTag is the context transition function for the tag state. |
func tTag(c context, s []byte) (context, int) { |
// Find the attribute name. |
i := eatWhiteSpace(s, 0) |
if i == len(s) { |
return c, len(s) |
} |
if s[i] == '>' { |
return context{ |
state: elementContentType[c.element], |
element: c.element, |
}, i + 1 |
} |
j, err := eatAttrName(s, i) |
if err != nil { |
return context{state: stateError, err: err}, len(s) |
} |
state, attr := stateTag, attrNone |
if i == j { |
return context{ |
state: stateError, |
err: errorf(ErrBadHTML, 0, "expected space, attr name, or end of tag, but got %q", s[i:]), |
}, len(s) |
} |
switch attrType(string(s[i:j])) { |
case contentTypeURL: |
attr = attrURL |
case contentTypeCSS: |
attr = attrStyle |
case contentTypeJS: |
attr = attrScript |
} |
if j == len(s) { |
state = stateAttrName |
} else { |
state = stateAfterName |
} |
return context{state: state, element: c.element, attr: attr}, j |
} |
|
// tAttrName is the context transition function for stateAttrName. |
func tAttrName(c context, s []byte) (context, int) { |
i, err := eatAttrName(s, 0) |
if err != nil { |
return context{state: stateError, err: err}, len(s) |
} else if i != len(s) { |
c.state = stateAfterName |
} |
return c, i |
} |
|
// tAfterName is the context transition function for stateAfterName. |
func tAfterName(c context, s []byte) (context, int) { |
// Look for the start of the value. |
i := eatWhiteSpace(s, 0) |
if i == len(s) { |
return c, len(s) |
} else if s[i] != '=' { |
// Occurs due to tag ending '>', and valueless attribute. |
c.state = stateTag |
return c, i |
} |
c.state = stateBeforeValue |
// Consume the "=". |
return c, i + 1 |
} |
|
var attrStartStates = [...]state{ |
attrNone: stateAttr, |
attrScript: stateJS, |
attrStyle: stateCSS, |
attrURL: stateURL, |
} |
|
// tBeforeValue is the context transition function for stateBeforeValue. |
func tBeforeValue(c context, s []byte) (context, int) { |
i := eatWhiteSpace(s, 0) |
if i == len(s) { |
return c, len(s) |
} |
// Find the attribute delimiter. |
delim := delimSpaceOrTagEnd |
switch s[i] { |
case '\'': |
delim, i = delimSingleQuote, i+1 |
case '"': |
delim, i = delimDoubleQuote, i+1 |
} |
c.state, c.delim, c.attr = attrStartStates[c.attr], delim, attrNone |
return c, i |
} |
|
// tHTMLCmt is the context transition function for stateHTMLCmt. |
func tHTMLCmt(c context, s []byte) (context, int) { |
if i := bytes.Index(s, commentEnd); i != -1 { |
return context{}, i + 3 |
} |
return c, len(s) |
} |
|
// specialTagEndMarkers maps element types to the character sequence that |
// case-insensitively signals the end of the special tag body. |
var specialTagEndMarkers = [...]string{ |
elementScript: "</script", |
elementStyle: "</style", |
elementTextarea: "</textarea", |
elementTitle: "</title", |
} |
|
// tSpecialTagEnd is the context transition function for raw text and RCDATA |
// element states. |
func tSpecialTagEnd(c context, s []byte) (context, int) { |
if c.element != elementNone { |
if i := strings.Index(strings.ToLower(string(s)), specialTagEndMarkers[c.element]); i != -1 { |
return context{}, i |
} |
} |
return c, len(s) |
} |
|
// tAttr is the context transition function for the attribute state. |
func tAttr(c context, s []byte) (context, int) { |
return c, len(s) |
} |
|
// tURL is the context transition function for the URL state. |
func tURL(c context, s []byte) (context, int) { |
if bytes.IndexAny(s, "#?") >= 0 { |
c.urlPart = urlPartQueryOrFrag |
} else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone { |
// HTML5 uses "Valid URL potentially surrounded by spaces" for |
// attrs: http://www.w3.org/TR/html5/index.html#attributes-1 |
c.urlPart = urlPartPreQuery |
} |
return c, len(s) |
} |
|
// tJS is the context transition function for the JS state. |
func tJS(c context, s []byte) (context, int) { |
i := bytes.IndexAny(s, `"'/`) |
if i == -1 { |
// Entire input is non string, comment, regexp tokens. |
c.jsCtx = nextJSCtx(s, c.jsCtx) |
return c, len(s) |
} |
c.jsCtx = nextJSCtx(s[:i], c.jsCtx) |
switch s[i] { |
case '"': |
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp |
case '\'': |
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp |
case '/': |
switch { |
case i+1 < len(s) && s[i+1] == '/': |
c.state, i = stateJSLineCmt, i+1 |
case i+1 < len(s) && s[i+1] == '*': |
c.state, i = stateJSBlockCmt, i+1 |
case c.jsCtx == jsCtxRegexp: |
c.state = stateJSRegexp |
case c.jsCtx == jsCtxDivOp: |
c.jsCtx = jsCtxRegexp |
default: |
return context{ |
state: stateError, |
err: errorf(ErrSlashAmbig, 0, "'/' could start a division or regexp: %.32q", s[i:]), |
}, len(s) |
} |
default: |
panic("unreachable") |
} |
return c, i + 1 |
} |
|
// tJSDelimited is the context transition function for the JS string and regexp |
// states. |
func tJSDelimited(c context, s []byte) (context, int) { |
specials := `\"` |
switch c.state { |
case stateJSSqStr: |
specials = `\'` |
case stateJSRegexp: |
specials = `\/[]` |
} |
|
k, inCharset := 0, false |
for { |
i := k + bytes.IndexAny(s[k:], specials) |
if i < k { |
break |
} |
switch s[i] { |
case '\\': |
i++ |
if i == len(s) { |
return context{ |
state: stateError, |
err: errorf(ErrPartialEscape, 0, "unfinished escape sequence in JS string: %q", s), |
}, len(s) |
} |
case '[': |
inCharset = true |
case ']': |
inCharset = false |
default: |
// end delimiter |
if !inCharset { |
c.state, c.jsCtx = stateJS, jsCtxDivOp |
return c, i + 1 |
} |
} |
k = i + 1 |
} |
|
if inCharset { |
// This can be fixed by making context richer if interpolation |
// into charsets is desired. |
return context{ |
state: stateError, |
err: errorf(ErrPartialCharset, 0, "unfinished JS regexp charset: %q", s), |
}, len(s) |
} |
|
return c, len(s) |
} |
|
var blockCommentEnd = []byte("*/") |
|
// tBlockCmt is the context transition function for /*comment*/ states. |
func tBlockCmt(c context, s []byte) (context, int) { |
i := bytes.Index(s, blockCommentEnd) |
if i == -1 { |
return c, len(s) |
} |
switch c.state { |
case stateJSBlockCmt: |
c.state = stateJS |
case stateCSSBlockCmt: |
c.state = stateCSS |
default: |
panic(c.state.String()) |
} |
return c, i + 2 |
} |
|
// tLineCmt is the context transition function for //comment states. |
func tLineCmt(c context, s []byte) (context, int) { |
var lineTerminators string |
var endState state |
switch c.state { |
case stateJSLineCmt: |
lineTerminators, endState = "\n\r\u2028\u2029", stateJS |
case stateCSSLineCmt: |
lineTerminators, endState = "\n\f\r", stateCSS |
// Line comments are not part of any published CSS standard but |
// are supported by the 4 major browsers. |
// This defines line comments as |
// LINECOMMENT ::= "//" [^\n\f\d]* |
// since http://www.w3.org/TR/css3-syntax/#SUBTOK-nl defines |
// newlines: |
// nl ::= #xA | #xD #xA | #xD | #xC |
default: |
panic(c.state.String()) |
} |
|
i := bytes.IndexAny(s, lineTerminators) |
if i == -1 { |
return c, len(s) |
} |
c.state = endState |
// Per section 7.4 of EcmaScript 5 : http://es5.github.com/#x7.4 |
// "However, the LineTerminator at the end of the line is not |
// considered to be part of the single-line comment; it is |
// recognized separately by the lexical grammar and becomes part |
// of the stream of input elements for the syntactic grammar." |
return c, i |
} |
|
// tCSS is the context transition function for the CSS state. |
func tCSS(c context, s []byte) (context, int) { |
// CSS quoted strings are almost never used except for: |
// (1) URLs as in background: "/foo.png" |
// (2) Multiword font-names as in font-family: "Times New Roman" |
// (3) List separators in content values as in inline-lists: |
// <style> |
// ul.inlineList { list-style: none; padding:0 } |
// ul.inlineList > li { display: inline } |
// ul.inlineList > li:before { content: ", " } |
// ul.inlineList > li:first-child:before { content: "" } |
// </style> |
// <ul class=inlineList><li>One<li>Two<li>Three</ul> |
// (4) Attribute value selectors as in a[href="http://example.com/"] |
// |
// We conservatively treat all strings as URLs, but make some |
// allowances to avoid confusion. |
// |
// In (1), our conservative assumption is justified. |
// In (2), valid font names do not contain ':', '?', or '#', so our |
// conservative assumption is fine since we will never transition past |
// urlPartPreQuery. |
// In (3), our protocol heuristic should not be tripped, and there |
// should not be non-space content after a '?' or '#', so as long as |
// we only %-encode RFC 3986 reserved characters we are ok. |
// In (4), we should URL escape for URL attributes, and for others we |
// have the attribute name available if our conservative assumption |
// proves problematic for real code. |
|
k := 0 |
for { |
i := k + bytes.IndexAny(s[k:], `("'/`) |
if i < k { |
return c, len(s) |
} |
switch s[i] { |
case '(': |
// Look for url to the left. |
p := bytes.TrimRight(s[:i], "\t\n\f\r ") |
if endsWithCSSKeyword(p, "url") { |
j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r ")) |
switch { |
case j != len(s) && s[j] == '"': |
c.state, j = stateCSSDqURL, j+1 |
case j != len(s) && s[j] == '\'': |
c.state, j = stateCSSSqURL, j+1 |
default: |
c.state = stateCSSURL |
} |
return c, j |
} |
case '/': |
if i+1 < len(s) { |
switch s[i+1] { |
case '/': |
c.state = stateCSSLineCmt |
return c, i + 2 |
case '*': |
c.state = stateCSSBlockCmt |
return c, i + 2 |
} |
} |
case '"': |
c.state = stateCSSDqStr |
return c, i + 1 |
case '\'': |
c.state = stateCSSSqStr |
return c, i + 1 |
} |
k = i + 1 |
} |
panic("unreachable") |
} |
|
// tCSSStr is the context transition function for the CSS string and URL states. |
func tCSSStr(c context, s []byte) (context, int) { |
var endAndEsc string |
switch c.state { |
case stateCSSDqStr, stateCSSDqURL: |
endAndEsc = `\"` |
case stateCSSSqStr, stateCSSSqURL: |
endAndEsc = `\'` |
case stateCSSURL: |
// Unquoted URLs end with a newline or close parenthesis. |
// The below includes the wc (whitespace character) and nl. |
endAndEsc = "\\\t\n\f\r )" |
default: |
panic(c.state.String()) |
} |
|
k := 0 |
for { |
i := k + bytes.IndexAny(s[k:], endAndEsc) |
if i < k { |
c, nread := tURL(c, decodeCSS(s[k:])) |
return c, k + nread |
} |
if s[i] == '\\' { |
i++ |
if i == len(s) { |
return context{ |
state: stateError, |
err: errorf(ErrPartialEscape, 0, "unfinished escape sequence in CSS string: %q", s), |
}, len(s) |
} |
} else { |
c.state = stateCSS |
return c, i + 1 |
} |
c, _ = tURL(c, decodeCSS(s[:i+1])) |
k = i + 1 |
} |
panic("unreachable") |
} |
|
// tError is the context transition function for the error state. |
func tError(c context, s []byte) (context, int) { |
return c, len(s) |
} |
|
// eatAttrName returns the largest j such that s[i:j] is an attribute name. |
// It returns an error if s[i:] does not look like it begins with an |
// attribute name, such as encountering a quote mark without a preceding |
// equals sign. |
func eatAttrName(s []byte, i int) (int, *Error) { |
for j := i; j < len(s); j++ { |
switch s[j] { |
case ' ', '\t', '\n', '\f', '\r', '=', '>': |
return j, nil |
case '\'', '"', '<': |
// These result in a parse warning in HTML5 and are |
// indicative of serious problems if seen in an attr |
// name in a template. |
return -1, errorf(ErrBadHTML, 0, "%q in attribute name: %.32q", s[j:j+1], s) |
default: |
// No-op. |
} |
} |
return len(s), nil |
} |
|
var elementNameMap = map[string]element{ |
"script": elementScript, |
"style": elementStyle, |
"textarea": elementTextarea, |
"title": elementTitle, |
} |
|
// asciiAlpha returns whether c is an ASCII letter. |
func asciiAlpha(c byte) bool { |
return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' |
} |
|
// asciiAlphaNum returns whether c is an ASCII letter or digit. |
func asciiAlphaNum(c byte) bool { |
return asciiAlpha(c) || '0' <= c && c <= '9' |
} |
|
// eatTagName returns the largest j such that s[i:j] is a tag name and the tag type. |
func eatTagName(s []byte, i int) (int, element) { |
if i == len(s) || !asciiAlpha(s[i]) { |
return i, elementNone |
} |
j := i + 1 |
for j < len(s) { |
x := s[j] |
if asciiAlphaNum(x) { |
j++ |
continue |
} |
// Allow "x-y" or "x:y" but not "x-", "-y", or "x--y". |
if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) { |
j += 2 |
continue |
} |
break |
} |
return j, elementNameMap[strings.ToLower(string(s[i:j]))] |
} |
|
// eatWhiteSpace returns the largest j such that s[i:j] is white space. |
func eatWhiteSpace(s []byte, i int) int { |
for j := i; j < len(s); j++ { |
switch s[j] { |
case ' ', '\t', '\n', '\f', '\r': |
// No-op. |
default: |
return j |
} |
} |
return len(s) |
} |
/template/escape_test.go
0,0 → 1,1643
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"encoding/json" |
"fmt" |
"strings" |
"testing" |
"text/template" |
"text/template/parse" |
) |
|
type badMarshaler struct{} |
|
func (x *badMarshaler) MarshalJSON() ([]byte, error) { |
// Keys in valid JSON must be double quoted as must all strings. |
return []byte("{ foo: 'not quite valid JSON' }"), nil |
} |
|
type goodMarshaler struct{} |
|
func (x *goodMarshaler) MarshalJSON() ([]byte, error) { |
return []byte(`{ "<foo>": "O'Reilly" }`), nil |
} |
|
func TestEscape(t *testing.T) { |
data := struct { |
F, T bool |
C, G, H string |
A, E []string |
B, M json.Marshaler |
N int |
Z *int |
W HTML |
}{ |
F: false, |
T: true, |
C: "<Cincinatti>", |
G: "<Goodbye>", |
H: "<Hello>", |
A: []string{"<a>", "<b>"}, |
E: []string{}, |
N: 42, |
B: &badMarshaler{}, |
M: &goodMarshaler{}, |
Z: nil, |
W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), |
} |
pdata := &data |
|
tests := []struct { |
name string |
input string |
output string |
}{ |
{ |
"if", |
"{{if .T}}Hello{{end}}, {{.C}}!", |
"Hello, <Cincinatti>!", |
}, |
{ |
"else", |
"{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", |
"<Goodbye>!", |
}, |
{ |
"overescaping1", |
"Hello, {{.C | html}}!", |
"Hello, <Cincinatti>!", |
}, |
{ |
"overescaping2", |
"Hello, {{html .C}}!", |
"Hello, <Cincinatti>!", |
}, |
{ |
"overescaping3", |
"{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", |
"Hello, <Cincinatti>!", |
}, |
{ |
"assignment", |
"{{if $x := .H}}{{$x}}{{end}}", |
"<Hello>", |
}, |
{ |
"withBody", |
"{{with .H}}{{.}}{{end}}", |
"<Hello>", |
}, |
{ |
"withElse", |
"{{with .E}}{{.}}{{else}}{{.H}}{{end}}", |
"<Hello>", |
}, |
{ |
"rangeBody", |
"{{range .A}}{{.}}{{end}}", |
"<a><b>", |
}, |
{ |
"rangeElse", |
"{{range .E}}{{.}}{{else}}{{.H}}{{end}}", |
"<Hello>", |
}, |
{ |
"nonStringValue", |
"{{.T}}", |
"true", |
}, |
{ |
"constant", |
`<a href="/search?q={{"'a<b'"}}">`, |
`<a href="/search?q=%27a%3cb%27">`, |
}, |
{ |
"multipleAttrs", |
"<a b=1 c={{.H}}>", |
"<a b=1 c=<Hello>>", |
}, |
{ |
"urlStartRel", |
`<a href='{{"/foo/bar?a=b&c=d"}}'>`, |
`<a href='/foo/bar?a=b&c=d'>`, |
}, |
{ |
"urlStartAbsOk", |
`<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, |
`<a href='http://example.com/foo/bar?a=b&c=d'>`, |
}, |
{ |
"protocolRelativeURLStart", |
`<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`, |
`<a href='//example.com:8000/foo/bar?a=b&c=d'>`, |
}, |
{ |
"pathRelativeURLStart", |
`<a href="{{"/javascript:80/foo/bar"}}">`, |
`<a href="/javascript:80/foo/bar">`, |
}, |
{ |
"dangerousURLStart", |
`<a href='{{"javascript:alert(%22pwned%22)"}}'>`, |
`<a href='#ZgotmplZ'>`, |
}, |
{ |
"dangerousURLStart2", |
`<a href=' {{"javascript:alert(%22pwned%22)"}}'>`, |
`<a href=' #ZgotmplZ'>`, |
}, |
{ |
"nonHierURL", |
`<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`, |
`<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`, |
}, |
{ |
"urlPath", |
`<a href='http://{{"javascript:80"}}/foo'>`, |
`<a href='http://javascript:80/foo'>`, |
}, |
{ |
"urlQuery", |
`<a href='/search?q={{.H}}'>`, |
`<a href='/search?q=%3cHello%3e'>`, |
}, |
{ |
"urlFragment", |
`<a href='/faq#{{.H}}'>`, |
`<a href='/faq#%3cHello%3e'>`, |
}, |
{ |
"urlBranch", |
`<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, |
`<a href="/bar">`, |
}, |
{ |
"urlBranchConflictMoot", |
`<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, |
`<a href="/foo?a=%3cCincinatti%3e">`, |
}, |
{ |
"jsStrValue", |
"<button onclick='alert({{.H}})'>", |
`<button onclick='alert("\u003cHello\u003e")'>`, |
}, |
{ |
"jsNumericValue", |
"<button onclick='alert({{.N}})'>", |
`<button onclick='alert( 42 )'>`, |
}, |
{ |
"jsBoolValue", |
"<button onclick='alert({{.T}})'>", |
`<button onclick='alert( true )'>`, |
}, |
{ |
"jsNilValue", |
"<button onclick='alert(typeof{{.Z}})'>", |
`<button onclick='alert(typeof null )'>`, |
}, |
{ |
"jsObjValue", |
"<button onclick='alert({{.A}})'>", |
`<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
}, |
{ |
"jsObjValueScript", |
"<script>alert({{.A}})</script>", |
`<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`, |
}, |
{ |
"jsObjValueNotOverEscaped", |
"<button onclick='alert({{.A | html}})'>", |
`<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
}, |
{ |
"jsStr", |
"<button onclick='alert("{{.H}}")'>", |
`<button onclick='alert("\x3cHello\x3e")'>`, |
}, |
{ |
"badMarshaller", |
`<button onclick='alert(1/{{.B}}in numbers)'>`, |
`<button onclick='alert(1/ /* json: error calling MarshalJSON for type *template.badMarshaler: invalid character 'f' looking for beginning of object key string */null in numbers)'>`, |
}, |
{ |
"jsMarshaller", |
`<button onclick='alert({{.M}})'>`, |
`<button onclick='alert({"<foo>":"O'Reilly"})'>`, |
}, |
{ |
"jsStrNotUnderEscaped", |
"<button onclick='alert({{.C | urlquery}})'>", |
// URL escaped, then quoted for JS. |
`<button onclick='alert("%3CCincinatti%3E")'>`, |
}, |
{ |
"jsRe", |
`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, |
`<button onclick='alert(/foo\x2bbar/.test(""))'>`, |
}, |
{ |
"jsReBlank", |
`<script>alert(/{{""}}/.test(""));</script>`, |
`<script>alert(/(?:)/.test(""));</script>`, |
}, |
{ |
"jsReAmbigOk", |
`<script>{{if true}}var x = 1{{end}}</script>`, |
// The {if} ends in an ambiguous jsCtx but there is |
// no slash following so we shouldn't care. |
`<script>var x = 1</script>`, |
}, |
{ |
"styleBidiKeywordPassed", |
`<p style="dir: {{"ltr"}}">`, |
`<p style="dir: ltr">`, |
}, |
{ |
"styleBidiPropNamePassed", |
`<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`, |
`<p style="border-left: 0; border-right: 1in">`, |
}, |
{ |
"styleExpressionBlocked", |
`<p style="width: {{"expression(alert(1337))"}}">`, |
`<p style="width: ZgotmplZ">`, |
}, |
{ |
"styleTagSelectorPassed", |
`<style>{{"p"}} { color: pink }</style>`, |
`<style>p { color: pink }</style>`, |
}, |
{ |
"styleIDPassed", |
`<style>p{{"#my-ID"}} { font: Arial }</style>`, |
`<style>p#my-ID { font: Arial }</style>`, |
}, |
{ |
"styleClassPassed", |
`<style>p{{".my_class"}} { font: Arial }</style>`, |
`<style>p.my_class { font: Arial }</style>`, |
}, |
{ |
"styleQuantityPassed", |
`<a style="left: {{"2em"}}; top: {{0}}">`, |
`<a style="left: 2em; top: 0">`, |
}, |
{ |
"stylePctPassed", |
`<table style=width:{{"100%"}}>`, |
`<table style=width:100%>`, |
}, |
{ |
"styleColorPassed", |
`<p style="color: {{"#8ff"}}; background: {{"#000"}}">`, |
`<p style="color: #8ff; background: #000">`, |
}, |
{ |
"styleObfuscatedExpressionBlocked", |
`<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`, |
`<p style="width: ZgotmplZ">`, |
}, |
{ |
"styleMozBindingBlocked", |
`<p style="{{"-moz-binding(alert(1337))"}}: ...">`, |
`<p style="ZgotmplZ: ...">`, |
}, |
{ |
"styleObfuscatedMozBindingBlocked", |
`<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`, |
`<p style="ZgotmplZ: ...">`, |
}, |
{ |
"styleFontNameString", |
`<p style='font-family: "{{"Times New Roman"}}"'>`, |
`<p style='font-family: "Times New Roman"'>`, |
}, |
{ |
"styleFontNameString", |
`<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`, |
`<p style='font-family: "Times New Roman", "sans-serif"'>`, |
}, |
{ |
"styleFontNameUnquoted", |
`<p style='font-family: {{"Times New Roman"}}'>`, |
`<p style='font-family: Times New Roman'>`, |
}, |
{ |
"styleURLQueryEncoded", |
`<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`, |
`<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`, |
}, |
{ |
"styleQuotedURLQueryEncoded", |
`<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`, |
`<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`, |
}, |
{ |
"styleStrQueryEncoded", |
`<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`, |
`<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`, |
}, |
{ |
"styleURLBadProtocolBlocked", |
`<a style="background: url('{{"javascript:alert(1337)"}}')">`, |
`<a style="background: url('#ZgotmplZ')">`, |
}, |
{ |
"styleStrBadProtocolBlocked", |
`<a style="background: '{{"vbscript:alert(1337)"}}'">`, |
`<a style="background: '#ZgotmplZ'">`, |
}, |
{ |
"styleStrEncodedProtocolEncoded", |
`<a style="background: '{{"javascript\\3a alert(1337)"}}'">`, |
// The CSS string 'javascript\\3a alert(1337)' does not contains a colon. |
`<a style="background: 'javascript\\3a alert\28 1337\29 '">`, |
}, |
{ |
"styleURLGoodProtocolPassed", |
`<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`, |
`<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`, |
}, |
{ |
"styleStrGoodProtocolPassed", |
`<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`, |
`<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`, |
}, |
{ |
"styleURLEncodedForHTMLInAttr", |
`<a style="background: url('{{"/search?img=foo&size=icon"}}')">`, |
`<a style="background: url('/search?img=foo&size=icon')">`, |
}, |
{ |
"styleURLNotEncodedForHTMLInCdata", |
`<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`, |
`<style>body { background: url('/search?img=foo&size=icon') }</style>`, |
}, |
{ |
"styleURLMixedCase", |
`<p style="background: URL(#{{.H}})">`, |
`<p style="background: URL(#%3cHello%3e)">`, |
}, |
{ |
"stylePropertyPairPassed", |
`<a style='{{"color: red"}}'>`, |
`<a style='color: red'>`, |
}, |
{ |
"styleStrSpecialsEncoded", |
`<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`, |
`<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`, |
}, |
{ |
"styleURLSpecialsEncoded", |
`<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`, |
`<a style="border-image: url(/**/%27%22;://%20%5c), url("/**/%27%22;://%20%5c"), url('/**/%27%22;://%20%5c'), 'http://www.example.com/?q=%2f%2a%2a%2f%27%22%3b%3a%2f%2f%20%5c''">`, |
}, |
{ |
"HTML comment", |
"<b>Hello, <!-- name of world -->{{.C}}</b>", |
"<b>Hello, <Cincinatti></b>", |
}, |
{ |
"HTML comment not first < in text node.", |
"<<!-- -->!--", |
"<!--", |
}, |
{ |
"HTML normalization 1", |
"a < b", |
"a < b", |
}, |
{ |
"HTML normalization 2", |
"a << b", |
"a << b", |
}, |
{ |
"HTML normalization 3", |
"a<<!-- --><!-- -->b", |
"a<b", |
}, |
{ |
"HTML doctype not normalized", |
"<!DOCTYPE html>Hello, World!", |
"<!DOCTYPE html>Hello, World!", |
}, |
{ |
"No doctype injection", |
`<!{{"DOCTYPE"}}`, |
"<!DOCTYPE", |
}, |
{ |
"Split HTML comment", |
"<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>", |
"<b>Hello, <Cincinatti></b>", |
}, |
{ |
"JS line comment", |
"<script>for (;;) { if (c()) break// foo not a label\n" + |
"foo({{.T}});}</script>", |
"<script>for (;;) { if (c()) break\n" + |
"foo( true );}</script>", |
}, |
{ |
"JS multiline block comment", |
"<script>for (;;) { if (c()) break/* foo not a label\n" + |
" */foo({{.T}});}</script>", |
// Newline separates break from call. If newline |
// removed, then break will consume label leaving |
// code invalid. |
"<script>for (;;) { if (c()) break\n" + |
"foo( true );}</script>", |
}, |
{ |
"JS single-line block comment", |
"<script>for (;;) {\n" + |
"if (c()) break/* foo a label */foo;" + |
"x({{.T}});}</script>", |
// Newline separates break from call. If newline |
// removed, then break will consume label leaving |
// code invalid. |
"<script>for (;;) {\n" + |
"if (c()) break foo;" + |
"x( true );}</script>", |
}, |
{ |
"JS block comment flush with mathematical division", |
"<script>var a/*b*//c\nd</script>", |
"<script>var a /c\nd</script>", |
}, |
{ |
"JS mixed comments", |
"<script>var a/*b*///c\nd</script>", |
"<script>var a \nd</script>", |
}, |
{ |
"CSS comments", |
"<style>p// paragraph\n" + |
`{border: 1px/* color */{{"#00f"}}}</style>`, |
"<style>p\n" + |
"{border: 1px #00f}</style>", |
}, |
{ |
"JS attr block comment", |
`<a onclick="f(""); /* alert({{.H}}) */">`, |
// Attribute comment tests should pass if the comments |
// are successfully elided. |
`<a onclick="f(""); /* alert() */">`, |
}, |
{ |
"JS attr line comment", |
`<a onclick="// alert({{.G}})">`, |
`<a onclick="// alert()">`, |
}, |
{ |
"CSS attr block comment", |
`<a style="/* color: {{.H}} */">`, |
`<a style="/* color: */">`, |
}, |
{ |
"CSS attr line comment", |
`<a style="// color: {{.G}}">`, |
`<a style="// color: ">`, |
}, |
{ |
"HTML substitution commented out", |
"<p><!-- {{.H}} --></p>", |
"<p></p>", |
}, |
{ |
"Comment ends flush with start", |
"<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>", |
"<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>", |
}, |
{ |
"typed HTML in text", |
`{{.W}}`, |
`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, |
}, |
{ |
"typed HTML in attribute", |
`<div title="{{.W}}">`, |
`<div title="¡Hello, O'World!">`, |
}, |
{ |
"typed HTML in script", |
`<button onclick="alert({{.W}})">`, |
`<button onclick="alert("&iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, |
}, |
{ |
"typed HTML in RCDATA", |
`<textarea>{{.W}}</textarea>`, |
`<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, |
}, |
{ |
"range in textarea", |
"<textarea>{{range .A}}{{.}}{{end}}</textarea>", |
"<textarea><a><b></textarea>", |
}, |
{ |
"auditable exemption from escaping", |
"{{range .A}}{{. | noescape}}{{end}}", |
"<a><b>", |
}, |
{ |
"No tag injection", |
`{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, |
`10$<script src,evil.org/pwnd.js...`, |
}, |
{ |
"No comment injection", |
`<{{"!--"}}`, |
`<!--`, |
}, |
{ |
"No RCDATA end tag injection", |
`<textarea><{{"/textarea "}}...</textarea>`, |
`<textarea></textarea ...</textarea>`, |
}, |
{ |
"optional attrs", |
`<img class="{{"iconClass"}}"` + |
`{{if .T}} id="{{"<iconId>"}}"{{end}}` + |
// Double quotes inside if/else. |
` src=` + |
`{{if .T}}"?{{"<iconPath>"}}"` + |
`{{else}}"images/cleardot.gif"{{end}}` + |
// Missing space before title, but it is not a |
// part of the src attribute. |
`{{if .T}}title="{{"<title>"}}"{{end}}` + |
// Quotes outside if/else. |
` alt="` + |
`{{if .T}}{{"<alt>"}}` + |
`{{else}}{{if .F}}{{"<title>"}}{{end}}` + |
`{{end}}"` + |
`>`, |
`<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, |
}, |
{ |
"conditional valueless attr name", |
`<input{{if .T}} checked{{end}} name=n>`, |
`<input checked name=n>`, |
}, |
{ |
"conditional dynamic valueless attr name 1", |
`<input{{if .T}} {{"checked"}}{{end}} name=n>`, |
`<input checked name=n>`, |
}, |
{ |
"conditional dynamic valueless attr name 2", |
`<input {{if .T}}{{"checked"}} {{end}}name=n>`, |
`<input checked name=n>`, |
}, |
{ |
"dynamic attribute name", |
`<img on{{"load"}}="alert({{"loaded"}})">`, |
// Treated as JS since quotes are inserted. |
`<img onload="alert("loaded")">`, |
}, |
{ |
"bad dynamic attribute name 1", |
// Allow checked, selected, disabled, but not JS or |
// CSS attributes. |
`<input {{"onchange"}}="{{"doEvil()"}}">`, |
`<input ZgotmplZ="doEvil()">`, |
}, |
{ |
"bad dynamic attribute name 2", |
`<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, |
`<div ZgotmplZ="color: expression(alert(1337))">`, |
}, |
{ |
"bad dynamic attribute name 3", |
// Allow title or alt, but not a URL. |
`<img {{"src"}}="{{"javascript:doEvil()"}}">`, |
`<img ZgotmplZ="javascript:doEvil()">`, |
}, |
{ |
"bad dynamic attribute name 4", |
// Structure preservation requires values to associate |
// with a consistent attribute. |
`<input checked {{""}}="Whose value am I?">`, |
`<input checked ZgotmplZ="Whose value am I?">`, |
}, |
{ |
"dynamic element name", |
`<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, |
`<h3><table><thead>...</h3>`, |
}, |
{ |
"bad dynamic element name", |
// Dynamic element names are typically used to switch |
// between (thead, tfoot, tbody), (ul, ol), (th, td), |
// and other replaceable sets. |
// We do not currently easily support (ul, ol). |
// If we do change to support that, this test should |
// catch failures to filter out special tag names which |
// would violate the structure preservation property -- |
// if any special tag name could be substituted, then |
// the content could be raw text/RCDATA for some inputs |
// and regular HTML content for others. |
`<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, |
`<script>doEvil()</script>`, |
}, |
} |
|
for _, test := range tests { |
tmpl := New(test.name) |
// TODO: Move noescape into template/func.go |
tmpl.Funcs(FuncMap{ |
"noescape": func(a ...interface{}) string { |
return fmt.Sprint(a...) |
}, |
}) |
tmpl = Must(tmpl.Parse(test.input)) |
b := new(bytes.Buffer) |
if err := tmpl.Execute(b, data); err != nil { |
t.Errorf("%s: template execution failed: %s", test.name, err) |
continue |
} |
if w, g := test.output, b.String(); w != g { |
t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) |
continue |
} |
b.Reset() |
if err := tmpl.Execute(b, pdata); err != nil { |
t.Errorf("%s: template execution failed for pointer: %s", test.name, err) |
continue |
} |
if w, g := test.output, b.String(); w != g { |
t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) |
continue |
} |
} |
} |
|
func TestEscapeSet(t *testing.T) { |
type dataItem struct { |
Children []*dataItem |
X string |
} |
|
data := dataItem{ |
Children: []*dataItem{ |
{X: "foo"}, |
{X: "<bar>"}, |
{ |
Children: []*dataItem{ |
{X: "baz"}, |
}, |
}, |
}, |
} |
|
tests := []struct { |
inputs map[string]string |
want string |
}{ |
// The trivial set. |
{ |
map[string]string{ |
"main": ``, |
}, |
``, |
}, |
// A template called in the start context. |
{ |
map[string]string{ |
"main": `Hello, {{template "helper"}}!`, |
// Not a valid top level HTML template. |
// "<b" is not a full tag. |
"helper": `{{"<World>"}}`, |
}, |
`Hello, <World>!`, |
}, |
// A template called in a context other than the start. |
{ |
map[string]string{ |
"main": `<a onclick='a = {{template "helper"}};'>`, |
// Not a valid top level HTML template. |
// "<b" is not a full tag. |
"helper": `{{"<a>"}}<b`, |
}, |
`<a onclick='a = "\u003ca\u003e"<b;'>`, |
}, |
// A recursive template that ends in its start context. |
{ |
map[string]string{ |
"main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, |
}, |
`foo <bar> baz `, |
}, |
// A recursive helper template that ends in its start context. |
{ |
map[string]string{ |
"main": `{{template "helper" .}}`, |
"helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, |
}, |
`<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, |
}, |
// Co-recursive templates that end in its start context. |
{ |
map[string]string{ |
"main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, |
"helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, |
}, |
`<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, |
}, |
// A template that is called in two different contexts. |
{ |
map[string]string{ |
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, |
"helper": `{{11}} of {{"<100>"}}`, |
}, |
`<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`, |
}, |
// A non-recursive template that ends in a different context. |
// helper starts in jsCtxRegexp and ends in jsCtxDivOp. |
{ |
map[string]string{ |
"main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, |
"helper": "{{126}}", |
}, |
`<script>var x= 126 /"42";</script>`, |
}, |
// A recursive template that ends in a similar context. |
{ |
map[string]string{ |
"main": `<script>var x=[{{template "countdown" 4}}];</script>`, |
"countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, |
}, |
`<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, |
}, |
// A recursive template that ends in a different context. |
/* |
{ |
map[string]string{ |
"main": `<a href="/foo{{template "helper" .}}">`, |
"helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, |
}, |
`<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, |
}, |
*/ |
} |
|
// pred is a template function that returns the predecessor of a |
// natural number for testing recursive templates. |
fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) { |
if len(a) == 1 { |
if i, _ := a[0].(int); i > 0 { |
return i - 1, nil |
} |
} |
return nil, fmt.Errorf("undefined pred(%v)", a) |
}} |
|
for _, test := range tests { |
source := "" |
for name, body := range test.inputs { |
source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) |
} |
tmpl, err := New("root").Funcs(fns).Parse(source) |
if err != nil { |
t.Errorf("error parsing %q: %v", source, err) |
continue |
} |
var b bytes.Buffer |
|
if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { |
t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) |
continue |
} |
if got := b.String(); test.want != got { |
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) |
} |
} |
|
} |
|
func TestErrors(t *testing.T) { |
tests := []struct { |
input string |
err string |
}{ |
// Non-error cases. |
{ |
"{{if .Cond}}<a>{{else}}<b>{{end}}", |
"", |
}, |
{ |
"{{if .Cond}}<a>{{end}}", |
"", |
}, |
{ |
"{{if .Cond}}{{else}}<b>{{end}}", |
"", |
}, |
{ |
"{{with .Cond}}<div>{{end}}", |
"", |
}, |
{ |
"{{range .Items}}<a>{{end}}", |
"", |
}, |
{ |
"<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", |
"", |
}, |
// Error cases. |
{ |
"{{if .Cond}}<a{{end}}", |
"z:1: {{if}} branches", |
}, |
{ |
"{{if .Cond}}\n{{else}}\n<a{{end}}", |
"z:1: {{if}} branches", |
}, |
{ |
// Missing quote in the else branch. |
`{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, |
"z:1: {{if}} branches", |
}, |
{ |
// Different kind of attribute: href implies a URL. |
"<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", |
"z:1: {{if}} branches", |
}, |
{ |
"\n{{with .X}}<a{{end}}", |
"z:2: {{with}} branches", |
}, |
{ |
"\n{{with .X}}<a>{{else}}<a{{end}}", |
"z:2: {{with}} branches", |
}, |
{ |
"{{range .Items}}<a{{end}}", |
`z:1: on range loop re-entry: "<" in attribute name: "<a"`, |
}, |
{ |
"\n{{range .Items}} x='<a{{end}}", |
"z:2: on range loop re-entry: {{range}} branches", |
}, |
{ |
"<a b=1 c={{.H}}", |
"z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", |
}, |
{ |
"<script>foo();", |
"z: ends in a non-text context: {stateJS", |
}, |
{ |
`<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, |
"z:1: {{.H}} appears in an ambiguous URL context", |
}, |
{ |
`<a onclick="alert('Hello \`, |
`unfinished escape sequence in JS string: "Hello \\"`, |
}, |
{ |
`<a onclick='alert("Hello\, World\`, |
`unfinished escape sequence in JS string: "Hello\\, World\\"`, |
}, |
{ |
`<a onclick='alert(/x+\`, |
`unfinished escape sequence in JS string: "x+\\"`, |
}, |
{ |
`<a onclick="/foo[\]/`, |
`unfinished JS regexp charset: "foo[\\]/"`, |
}, |
{ |
// It is ambiguous whether 1.5 should be 1\.5 or 1.5. |
// Either `var x = 1/- 1.5 /i.test(x)` |
// where `i.test(x)` is a method call of reference i, |
// or `/-1\.5/i.test(x)` which is a method call on a |
// case insensitive regular expression. |
`<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, |
`'/' could start a division or regexp: "/-"`, |
}, |
{ |
`{{template "foo"}}`, |
"z:1: no such template \"foo\"", |
}, |
{ |
`<div{{template "y"}}>` + |
// Illegal starting in stateTag but not in stateText. |
`{{define "y"}} foo<b{{end}}`, |
`"<" in attribute name: " foo<b"`, |
}, |
{ |
`<script>reverseList = [{{template "t"}}]</script>` + |
// Missing " after recursive call. |
`{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, |
`: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, |
}, |
{ |
`<input type=button value=onclick=>`, |
`html/template:z: "=" in unquoted attr: "onclick="`, |
}, |
{ |
`<input type=button value= onclick=>`, |
`html/template:z: "=" in unquoted attr: "onclick="`, |
}, |
{ |
`<input type=button value= 1+1=2>`, |
`html/template:z: "=" in unquoted attr: "1+1=2"`, |
}, |
{ |
"<a class=`foo>", |
"html/template:z: \"`\" in unquoted attr: \"`foo\"", |
}, |
{ |
`<a style=font:'Arial'>`, |
`html/template:z: "'" in unquoted attr: "font:'Arial'"`, |
}, |
{ |
`<a=foo>`, |
`: expected space, attr name, or end of tag, but got "=foo>"`, |
}, |
} |
|
for _, test := range tests { |
buf := new(bytes.Buffer) |
tmpl, err := New("z").Parse(test.input) |
if err != nil { |
t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) |
continue |
} |
err = tmpl.Execute(buf, nil) |
var got string |
if err != nil { |
got = err.Error() |
} |
if test.err == "" { |
if got != "" { |
t.Errorf("input=%q: unexpected error %q", test.input, got) |
} |
continue |
} |
if strings.Index(got, test.err) == -1 { |
t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) |
continue |
} |
} |
} |
|
func TestEscapeText(t *testing.T) { |
tests := []struct { |
input string |
output context |
}{ |
{ |
``, |
context{}, |
}, |
{ |
`Hello, World!`, |
context{}, |
}, |
{ |
// An orphaned "<" is OK. |
`I <3 Ponies!`, |
context{}, |
}, |
{ |
`<a`, |
context{state: stateTag}, |
}, |
{ |
`<a `, |
context{state: stateTag}, |
}, |
{ |
`<a>`, |
context{state: stateText}, |
}, |
{ |
`<a href`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a on`, |
context{state: stateAttrName, attr: attrScript}, |
}, |
{ |
`<a href `, |
context{state: stateAfterName, attr: attrURL}, |
}, |
{ |
`<a style = `, |
context{state: stateBeforeValue, attr: attrStyle}, |
}, |
{ |
`<a href=`, |
context{state: stateBeforeValue, attr: attrURL}, |
}, |
{ |
`<a href=x`, |
context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href=x `, |
context{state: stateTag}, |
}, |
{ |
`<a href=>`, |
context{state: stateText}, |
}, |
{ |
`<a href=x>`, |
context{state: stateText}, |
}, |
{ |
`<a href ='`, |
context{state: stateURL, delim: delimSingleQuote}, |
}, |
{ |
`<a href=''`, |
context{state: stateTag}, |
}, |
{ |
`<a href= "`, |
context{state: stateURL, delim: delimDoubleQuote}, |
}, |
{ |
`<a href=""`, |
context{state: stateTag}, |
}, |
{ |
`<a title="`, |
context{state: stateAttr, delim: delimDoubleQuote}, |
}, |
{ |
`<a HREF='http:`, |
context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a Href='/`, |
context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href='"`, |
context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href="'`, |
context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href=''`, |
context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href=""`, |
context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href=""`, |
context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a href="`, |
context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, |
}, |
{ |
`<img alt="1">`, |
context{state: stateText}, |
}, |
{ |
`<img alt="1>"`, |
context{state: stateTag}, |
}, |
{ |
`<img alt="1>">`, |
context{state: stateText}, |
}, |
{ |
`<input checked type="checkbox"`, |
context{state: stateTag}, |
}, |
{ |
`<a onclick="`, |
context{state: stateJS, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="//foo`, |
context{state: stateJSLineCmt, delim: delimDoubleQuote}, |
}, |
{ |
"<a onclick='//\n", |
context{state: stateJS, delim: delimSingleQuote}, |
}, |
{ |
"<a onclick='//\r\n", |
context{state: stateJS, delim: delimSingleQuote}, |
}, |
{ |
"<a onclick='//\u2028", |
context{state: stateJS, delim: delimSingleQuote}, |
}, |
{ |
`<a onclick="/*`, |
context{state: stateJSBlockCmt, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/*/`, |
context{state: stateJSBlockCmt, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/**/`, |
context{state: stateJS, delim: delimDoubleQuote}, |
}, |
{ |
`<a onkeypress=""`, |
context{state: stateJSDqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick='"foo"`, |
context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<a onclick='foo'`, |
context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<a onclick='foo`, |
context{state: stateJSSqStr, delim: delimSpaceOrTagEnd}, |
}, |
{ |
`<a onclick=""foo'`, |
context{state: stateJSDqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="'foo"`, |
context{state: stateJSSqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<A ONCLICK="'`, |
context{state: stateJSSqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/`, |
context{state: stateJSRegexp, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="'foo'`, |
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<a onclick="'foo\'`, |
context{state: stateJSSqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="'foo\'`, |
context{state: stateJSSqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/foo/`, |
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<script>/foo/ /=`, |
context{state: stateJS, element: elementScript}, |
}, |
{ |
`<a onclick="1 /foo`, |
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<a onclick="1 /*c*/ /foo`, |
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<a onclick="/foo[/]`, |
context{state: stateJSRegexp, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/foo\/`, |
context{state: stateJSRegexp, delim: delimDoubleQuote}, |
}, |
{ |
`<a onclick="/foo/`, |
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
}, |
{ |
`<input checked style="`, |
context{state: stateCSS, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="//`, |
context{state: stateCSSLineCmt, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="//</script>`, |
context{state: stateCSSLineCmt, delim: delimDoubleQuote}, |
}, |
{ |
"<a style='//\n", |
context{state: stateCSS, delim: delimSingleQuote}, |
}, |
{ |
"<a style='//\r", |
context{state: stateCSS, delim: delimSingleQuote}, |
}, |
{ |
`<a style="/*`, |
context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="/*/`, |
context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="/**/`, |
context{state: stateCSS, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: '`, |
context{state: stateCSSSqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: "`, |
context{state: stateCSSDqStr, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: '/foo?img=`, |
context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, |
}, |
{ |
`<a style="background: '/`, |
context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url("/`, |
context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url('/`, |
context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url('/)`, |
context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url('/ `, |
context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url(/`, |
context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
}, |
{ |
`<a style="background: url( `, |
context{state: stateCSSURL, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: url( /image?name=`, |
context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, |
}, |
{ |
`<a style="background: url(x)`, |
context{state: stateCSS, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: url('x'`, |
context{state: stateCSS, delim: delimDoubleQuote}, |
}, |
{ |
`<a style="background: url( x `, |
context{state: stateCSS, delim: delimDoubleQuote}, |
}, |
{ |
`<!-- foo`, |
context{state: stateHTMLCmt}, |
}, |
{ |
`<!-->`, |
context{state: stateHTMLCmt}, |
}, |
{ |
`<!--->`, |
context{state: stateHTMLCmt}, |
}, |
{ |
`<!-- foo -->`, |
context{state: stateText}, |
}, |
{ |
`<script`, |
context{state: stateTag, element: elementScript}, |
}, |
{ |
`<script `, |
context{state: stateTag, element: elementScript}, |
}, |
{ |
`<script src="foo.js" `, |
context{state: stateTag, element: elementScript}, |
}, |
{ |
`<script src='foo.js' `, |
context{state: stateTag, element: elementScript}, |
}, |
{ |
`<script type=text/javascript `, |
context{state: stateTag, element: elementScript}, |
}, |
{ |
`<script>foo`, |
context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, |
}, |
{ |
`<script>foo</script>`, |
context{state: stateText}, |
}, |
{ |
`<script>foo</script><!--`, |
context{state: stateHTMLCmt}, |
}, |
{ |
`<script>document.write("<p>foo</p>");`, |
context{state: stateJS, element: elementScript}, |
}, |
{ |
`<script>document.write("<p>foo<\/script>");`, |
context{state: stateJS, element: elementScript}, |
}, |
{ |
`<script>document.write("<script>alert(1)</script>");`, |
context{state: stateText}, |
}, |
{ |
`<Script>`, |
context{state: stateJS, element: elementScript}, |
}, |
{ |
`<SCRIPT>foo`, |
context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, |
}, |
{ |
`<textarea>value`, |
context{state: stateRCDATA, element: elementTextarea}, |
}, |
{ |
`<textarea>value</TEXTAREA>`, |
context{state: stateText}, |
}, |
{ |
`<textarea name=html><b`, |
context{state: stateRCDATA, element: elementTextarea}, |
}, |
{ |
`<title>value`, |
context{state: stateRCDATA, element: elementTitle}, |
}, |
{ |
`<style>value`, |
context{state: stateCSS, element: elementStyle}, |
}, |
{ |
`<a xlink:href`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a xmlns`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a xmlns:foo`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a xmlnsxyz`, |
context{state: stateAttrName}, |
}, |
{ |
`<a data-url`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a data-iconUri`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a data-urlItem`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a g:`, |
context{state: stateAttrName}, |
}, |
{ |
`<a g:url`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a g:iconUri`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a g:urlItem`, |
context{state: stateAttrName, attr: attrURL}, |
}, |
{ |
`<a g:value`, |
context{state: stateAttrName}, |
}, |
{ |
`<a svg:style='`, |
context{state: stateCSS, delim: delimSingleQuote}, |
}, |
{ |
`<svg:font-face`, |
context{state: stateTag}, |
}, |
{ |
`<svg:a svg:onclick="`, |
context{state: stateJS, delim: delimDoubleQuote}, |
}, |
} |
|
for _, test := range tests { |
b, e := []byte(test.input), newEscaper(nil) |
c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) |
if !test.output.eq(c) { |
t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) |
continue |
} |
if test.input != string(b) { |
t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) |
continue |
} |
} |
} |
|
func TestEnsurePipelineContains(t *testing.T) { |
tests := []struct { |
input, output string |
ids []string |
}{ |
{ |
"{{.X}}", |
".X", |
[]string{}, |
}, |
{ |
"{{.X | html}}", |
".X | html", |
[]string{}, |
}, |
{ |
"{{.X}}", |
".X | html", |
[]string{"html"}, |
}, |
{ |
"{{.X | html}}", |
".X | html | urlquery", |
[]string{"urlquery"}, |
}, |
{ |
"{{.X | html | urlquery}}", |
".X | html | urlquery", |
[]string{"urlquery"}, |
}, |
{ |
"{{.X | html | urlquery}}", |
".X | html | urlquery", |
[]string{"html", "urlquery"}, |
}, |
{ |
"{{.X | html | urlquery}}", |
".X | html | urlquery", |
[]string{"html"}, |
}, |
{ |
"{{.X | urlquery}}", |
".X | html | urlquery", |
[]string{"html", "urlquery"}, |
}, |
{ |
"{{.X | html | print}}", |
".X | urlquery | html | print", |
[]string{"urlquery", "html"}, |
}, |
} |
for i, test := range tests { |
tmpl := template.Must(template.New("test").Parse(test.input)) |
action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) |
if !ok { |
t.Errorf("#%d: First node is not an action: %s", i, test.input) |
continue |
} |
pipe := action.Pipe |
ensurePipelineContains(pipe, test.ids) |
got := pipe.String() |
if got != test.output { |
t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got) |
} |
} |
} |
|
func TestEscapeErrorsNotIgnorable(t *testing.T) { |
var b bytes.Buffer |
tmpl, _ := New("dangerous").Parse("<a") |
err := tmpl.Execute(&b, nil) |
if err == nil { |
t.Errorf("Expected error") |
} else if b.Len() != 0 { |
t.Errorf("Emitted output despite escaping failure") |
} |
} |
|
func TestEscapeSetErrorsNotIgnorable(t *testing.T) { |
var b bytes.Buffer |
tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) |
if err != nil { |
t.Errorf("failed to parse set: %q", err) |
} |
err = tmpl.ExecuteTemplate(&b, "t", nil) |
if err == nil { |
t.Errorf("Expected error") |
} else if b.Len() != 0 { |
t.Errorf("Emitted output despite escaping failure") |
} |
} |
|
func TestRedundantFuncs(t *testing.T) { |
inputs := []interface{}{ |
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + |
"&%22\\", |
CSS(`a[href =~ "//example.com"]#foo`), |
HTML(`Hello, <b>World</b> &tc!`), |
HTMLAttr(` dir="ltr"`), |
JS(`c && alert("Hello, World!");`), |
JSStr(`Hello, World & O'Reilly\x21`), |
URL(`greeting=H%69&addressee=(World)`), |
} |
|
for n0, m := range redundantFuncs { |
f0 := funcMap[n0].(func(...interface{}) string) |
for n1 := range m { |
f1 := funcMap[n1].(func(...interface{}) string) |
for _, input := range inputs { |
want := f0(input) |
if got := f1(want); want != got { |
t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) |
} |
} |
} |
} |
} |
|
func TestIndirectPrint(t *testing.T) { |
a := 3 |
ap := &a |
b := "hello" |
bp := &b |
bpp := &bp |
tmpl := Must(New("t").Parse(`{{.}}`)) |
var buf bytes.Buffer |
err := tmpl.Execute(&buf, ap) |
if err != nil { |
t.Errorf("Unexpected error: %s", err) |
} else if buf.String() != "3" { |
t.Errorf(`Expected "3"; got %q`, buf.String()) |
} |
buf.Reset() |
err = tmpl.Execute(&buf, bpp) |
if err != nil { |
t.Errorf("Unexpected error: %s", err) |
} else if buf.String() != "hello" { |
t.Errorf(`Expected "hello"; got %q`, buf.String()) |
} |
} |
|
func BenchmarkEscapedExecute(b *testing.B) { |
tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) |
var buf bytes.Buffer |
b.ResetTimer() |
for i := 0; i < b.N; i++ { |
tmpl.Execute(&buf, "foo & 'bar' & baz") |
buf.Reset() |
} |
} |
/template/clone_test.go
0,0 → 1,92
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"testing" |
) |
|
func TestClone(t *testing.T) { |
tests := []struct { |
input, want, wantClone string |
}{ |
{ |
`Hello, {{if true}}{{"<World>"}}{{end}}!`, |
"Hello, <World>!", |
"Hello, <World>!", |
}, |
{ |
`Hello, {{if false}}{{.X}}{{else}}{{"<World>"}}{{end}}!`, |
"Hello, <World>!", |
"Hello, <World>!", |
}, |
{ |
`Hello, {{with "<World>"}}{{.}}{{end}}!`, |
"Hello, <World>!", |
"Hello, <World>!", |
}, |
{ |
`{{range .}}<p>{{.}}</p>{{end}}`, |
"<p>foo</p><p><bar></p><p>baz</p>", |
"<p>foo</p><p><bar></p><p>baz</p>", |
}, |
{ |
`Hello, {{"<World>" | html}}!`, |
"Hello, <World>!", |
"Hello, <World>!", |
}, |
{ |
`Hello{{if 1}}, World{{else}}{{template "d"}}{{end}}!`, |
"Hello, World!", |
"Hello, World!", |
}, |
} |
|
for _, test := range tests { |
s, err := New("s").Parse(test.input) |
if err != nil { |
t.Errorf("input=%q: unexpected parse error %v", test.input, err) |
} |
|
d, _ := New("d").Parse(test.input) |
// Hack: just replace the root of the tree. |
d.text.Root = cloneList(s.text.Root) |
|
if want, got := s.text.Root.String(), d.text.Root.String(); want != got { |
t.Errorf("want %q, got %q", want, got) |
} |
|
err = escapeTemplates(d, "d") |
if err != nil { |
t.Errorf("%q: failed to escape: %s", test.input, err) |
continue |
} |
|
if want, got := "s", s.Name(); want != got { |
t.Errorf("want %q, got %q", want, got) |
continue |
} |
if want, got := "d", d.Name(); want != got { |
t.Errorf("want %q, got %q", want, got) |
continue |
} |
|
data := []string{"foo", "<bar>", "baz"} |
|
var b bytes.Buffer |
d.Execute(&b, data) |
if got := b.String(); got != test.wantClone { |
t.Errorf("input=%q: want %q, got %q", test.input, test.wantClone, got) |
} |
|
// Make sure escaping d did not affect s. |
b.Reset() |
s.text.Execute(&b, data) |
if got := b.String(); got != test.want { |
t.Errorf("input=%q: want %q, got %q", test.input, test.want, got) |
} |
} |
} |
/template/escape.go
0,0 → 1,753
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"fmt" |
"html" |
"text/template" |
"text/template/parse" |
) |
|
// escapeTemplates rewrites the named templates, which must be |
// associated with t, to guarantee that the output of any of the named |
// templates is properly escaped. Names should include the names of |
// all templates that might be Executed but need not include helper |
// templates. If no error is returned, then the named templates have |
// been modified. Otherwise the named templates have been rendered |
// unusable. |
func escapeTemplates(tmpl *Template, names ...string) error { |
e := newEscaper(tmpl) |
for _, name := range names { |
c, _ := e.escapeTree(context{}, name, 0) |
var err error |
if c.err != nil { |
err, c.err.Name = c.err, name |
} else if c.state != stateText { |
err = &Error{ErrEndContext, name, 0, fmt.Sprintf("ends in a non-text context: %v", c)} |
} |
if err != nil { |
// Prevent execution of unsafe templates. |
for _, name := range names { |
if t := tmpl.set[name]; t != nil { |
t.text.Tree = nil |
} |
} |
return err |
} |
tmpl.escaped = true |
} |
e.commit() |
return nil |
} |
|
// funcMap maps command names to functions that render their inputs safe. |
var funcMap = template.FuncMap{ |
"exp_template_html_attrescaper": attrEscaper, |
"exp_template_html_commentescaper": commentEscaper, |
"exp_template_html_cssescaper": cssEscaper, |
"exp_template_html_cssvaluefilter": cssValueFilter, |
"exp_template_html_htmlnamefilter": htmlNameFilter, |
"exp_template_html_htmlescaper": htmlEscaper, |
"exp_template_html_jsregexpescaper": jsRegexpEscaper, |
"exp_template_html_jsstrescaper": jsStrEscaper, |
"exp_template_html_jsvalescaper": jsValEscaper, |
"exp_template_html_nospaceescaper": htmlNospaceEscaper, |
"exp_template_html_rcdataescaper": rcdataEscaper, |
"exp_template_html_urlescaper": urlEscaper, |
"exp_template_html_urlfilter": urlFilter, |
"exp_template_html_urlnormalizer": urlNormalizer, |
} |
|
// equivEscapers matches contextual escapers to equivalent template builtins. |
var equivEscapers = map[string]string{ |
"exp_template_html_attrescaper": "html", |
"exp_template_html_htmlescaper": "html", |
"exp_template_html_nospaceescaper": "html", |
"exp_template_html_rcdataescaper": "html", |
"exp_template_html_urlescaper": "urlquery", |
"exp_template_html_urlnormalizer": "urlquery", |
} |
|
// escaper collects type inferences about templates and changes needed to make |
// templates injection safe. |
type escaper struct { |
tmpl *Template |
// output[templateName] is the output context for a templateName that |
// has been mangled to include its input context. |
output map[string]context |
// derived[c.mangle(name)] maps to a template derived from the template |
// named name templateName for the start context c. |
derived map[string]*template.Template |
// called[templateName] is a set of called mangled template names. |
called map[string]bool |
// xxxNodeEdits are the accumulated edits to apply during commit. |
// Such edits are not applied immediately in case a template set |
// executes a given template in different escaping contexts. |
actionNodeEdits map[*parse.ActionNode][]string |
templateNodeEdits map[*parse.TemplateNode]string |
textNodeEdits map[*parse.TextNode][]byte |
} |
|
// newEscaper creates a blank escaper for the given set. |
func newEscaper(t *Template) *escaper { |
return &escaper{ |
t, |
map[string]context{}, |
map[string]*template.Template{}, |
map[string]bool{}, |
map[*parse.ActionNode][]string{}, |
map[*parse.TemplateNode]string{}, |
map[*parse.TextNode][]byte{}, |
} |
} |
|
// filterFailsafe is an innocuous word that is emitted in place of unsafe values |
// by sanitizer functions. It is not a keyword in any programming language, |
// contains no special characters, is not empty, and when it appears in output |
// it is distinct enough that a developer can find the source of the problem |
// via a search engine. |
const filterFailsafe = "ZgotmplZ" |
|
// escape escapes a template node. |
func (e *escaper) escape(c context, n parse.Node) context { |
switch n := n.(type) { |
case *parse.ActionNode: |
return e.escapeAction(c, n) |
case *parse.IfNode: |
return e.escapeBranch(c, &n.BranchNode, "if") |
case *parse.ListNode: |
return e.escapeList(c, n) |
case *parse.RangeNode: |
return e.escapeBranch(c, &n.BranchNode, "range") |
case *parse.TemplateNode: |
return e.escapeTemplate(c, n) |
case *parse.TextNode: |
return e.escapeText(c, n) |
case *parse.WithNode: |
return e.escapeBranch(c, &n.BranchNode, "with") |
} |
panic("escaping " + n.String() + " is unimplemented") |
} |
|
// escapeAction escapes an action template node. |
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { |
if len(n.Pipe.Decl) != 0 { |
// A local variable assignment, not an interpolation. |
return c |
} |
c = nudge(c) |
s := make([]string, 0, 3) |
switch c.state { |
case stateError: |
return c |
case stateURL, stateCSSDqStr, stateCSSSqStr, stateCSSDqURL, stateCSSSqURL, stateCSSURL: |
switch c.urlPart { |
case urlPartNone: |
s = append(s, "exp_template_html_urlfilter") |
fallthrough |
case urlPartPreQuery: |
switch c.state { |
case stateCSSDqStr, stateCSSSqStr: |
s = append(s, "exp_template_html_cssescaper") |
default: |
s = append(s, "exp_template_html_urlnormalizer") |
} |
case urlPartQueryOrFrag: |
s = append(s, "exp_template_html_urlescaper") |
case urlPartUnknown: |
return context{ |
state: stateError, |
err: errorf(ErrAmbigContext, n.Line, "%s appears in an ambiguous URL context", n), |
} |
default: |
panic(c.urlPart.String()) |
} |
case stateJS: |
s = append(s, "exp_template_html_jsvalescaper") |
// A slash after a value starts a div operator. |
c.jsCtx = jsCtxDivOp |
case stateJSDqStr, stateJSSqStr: |
s = append(s, "exp_template_html_jsstrescaper") |
case stateJSRegexp: |
s = append(s, "exp_template_html_jsregexpescaper") |
case stateCSS: |
s = append(s, "exp_template_html_cssvaluefilter") |
case stateText: |
s = append(s, "exp_template_html_htmlescaper") |
case stateRCDATA: |
s = append(s, "exp_template_html_rcdataescaper") |
case stateAttr: |
// Handled below in delim check. |
case stateAttrName, stateTag: |
c.state = stateAttrName |
s = append(s, "exp_template_html_htmlnamefilter") |
default: |
if isComment(c.state) { |
s = append(s, "exp_template_html_commentescaper") |
} else { |
panic("unexpected state " + c.state.String()) |
} |
} |
switch c.delim { |
case delimNone: |
// No extra-escaping needed for raw text content. |
case delimSpaceOrTagEnd: |
s = append(s, "exp_template_html_nospaceescaper") |
default: |
s = append(s, "exp_template_html_attrescaper") |
} |
e.editActionNode(n, s) |
return c |
} |
|
// ensurePipelineContains ensures that the pipeline has commands with |
// the identifiers in s in order. |
// If the pipeline already has some of the sanitizers, do not interfere. |
// For example, if p is (.X | html) and s is ["escapeJSVal", "html"] then it |
// has one matching, "html", and one to insert, "escapeJSVal", to produce |
// (.X | escapeJSVal | html). |
func ensurePipelineContains(p *parse.PipeNode, s []string) { |
if len(s) == 0 { |
return |
} |
n := len(p.Cmds) |
// Find the identifiers at the end of the command chain. |
idents := p.Cmds |
for i := n - 1; i >= 0; i-- { |
if cmd := p.Cmds[i]; len(cmd.Args) != 0 { |
if id, ok := cmd.Args[0].(*parse.IdentifierNode); ok { |
if id.Ident == "noescape" { |
return |
} |
continue |
} |
} |
idents = p.Cmds[i+1:] |
} |
dups := 0 |
for _, id := range idents { |
if escFnsEq(s[dups], (id.Args[0].(*parse.IdentifierNode)).Ident) { |
dups++ |
if dups == len(s) { |
return |
} |
} |
} |
newCmds := make([]*parse.CommandNode, n-len(idents), n+len(s)-dups) |
copy(newCmds, p.Cmds) |
// Merge existing identifier commands with the sanitizers needed. |
for _, id := range idents { |
i := indexOfStr((id.Args[0].(*parse.IdentifierNode)).Ident, s, escFnsEq) |
if i != -1 { |
for _, name := range s[:i] { |
newCmds = appendCmd(newCmds, newIdentCmd(name)) |
} |
s = s[i+1:] |
} |
newCmds = appendCmd(newCmds, id) |
} |
// Create any remaining sanitizers. |
for _, name := range s { |
newCmds = appendCmd(newCmds, newIdentCmd(name)) |
} |
p.Cmds = newCmds |
} |
|
// redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x) |
// for all x. |
var redundantFuncs = map[string]map[string]bool{ |
"exp_template_html_commentescaper": { |
"exp_template_html_attrescaper": true, |
"exp_template_html_nospaceescaper": true, |
"exp_template_html_htmlescaper": true, |
}, |
"exp_template_html_cssescaper": { |
"exp_template_html_attrescaper": true, |
}, |
"exp_template_html_jsregexpescaper": { |
"exp_template_html_attrescaper": true, |
}, |
"exp_template_html_jsstrescaper": { |
"exp_template_html_attrescaper": true, |
}, |
"exp_template_html_urlescaper": { |
"exp_template_html_urlnormalizer": true, |
}, |
} |
|
// appendCmd appends the given command to the end of the command pipeline |
// unless it is redundant with the last command. |
func appendCmd(cmds []*parse.CommandNode, cmd *parse.CommandNode) []*parse.CommandNode { |
if n := len(cmds); n != 0 { |
last, ok := cmds[n-1].Args[0].(*parse.IdentifierNode) |
next, _ := cmd.Args[0].(*parse.IdentifierNode) |
if ok && redundantFuncs[last.Ident][next.Ident] { |
return cmds |
} |
} |
return append(cmds, cmd) |
} |
|
// indexOfStr is the first i such that eq(s, strs[i]) or -1 if s was not found. |
func indexOfStr(s string, strs []string, eq func(a, b string) bool) int { |
for i, t := range strs { |
if eq(s, t) { |
return i |
} |
} |
return -1 |
} |
|
// escFnsEq returns whether the two escaping functions are equivalent. |
func escFnsEq(a, b string) bool { |
if e := equivEscapers[a]; e != "" { |
a = e |
} |
if e := equivEscapers[b]; e != "" { |
b = e |
} |
return a == b |
} |
|
// newIdentCmd produces a command containing a single identifier node. |
func newIdentCmd(identifier string) *parse.CommandNode { |
return &parse.CommandNode{ |
NodeType: parse.NodeCommand, |
Args: []parse.Node{parse.NewIdentifier(identifier)}, |
} |
} |
|
// nudge returns the context that would result from following empty string |
// transitions from the input context. |
// For example, parsing: |
// `<a href=` |
// will end in context{stateBeforeValue, attrURL}, but parsing one extra rune: |
// `<a href=x` |
// will end in context{stateURL, delimSpaceOrTagEnd, ...}. |
// There are two transitions that happen when the 'x' is seen: |
// (1) Transition from a before-value state to a start-of-value state without |
// consuming any character. |
// (2) Consume 'x' and transition past the first value character. |
// In this case, nudging produces the context after (1) happens. |
func nudge(c context) context { |
switch c.state { |
case stateTag: |
// In `<foo {{.}}`, the action should emit an attribute. |
c.state = stateAttrName |
case stateBeforeValue: |
// In `<foo bar={{.}}`, the action is an undelimited value. |
c.state, c.delim, c.attr = attrStartStates[c.attr], delimSpaceOrTagEnd, attrNone |
case stateAfterName: |
// In `<foo bar {{.}}`, the action is an attribute name. |
c.state, c.attr = stateAttrName, attrNone |
} |
return c |
} |
|
// join joins the two contexts of a branch template node. The result is an |
// error context if either of the input contexts are error contexts, or if the |
// the input contexts differ. |
func join(a, b context, line int, nodeName string) context { |
if a.state == stateError { |
return a |
} |
if b.state == stateError { |
return b |
} |
if a.eq(b) { |
return a |
} |
|
c := a |
c.urlPart = b.urlPart |
if c.eq(b) { |
// The contexts differ only by urlPart. |
c.urlPart = urlPartUnknown |
return c |
} |
|
c = a |
c.jsCtx = b.jsCtx |
if c.eq(b) { |
// The contexts differ only by jsCtx. |
c.jsCtx = jsCtxUnknown |
return c |
} |
|
// Allow a nudged context to join with an unnudged one. |
// This means that |
// <p title={{if .C}}{{.}}{{end}} |
// ends in an unquoted value state even though the else branch |
// ends in stateBeforeValue. |
if c, d := nudge(a), nudge(b); !(c.eq(a) && d.eq(b)) { |
if e := join(c, d, line, nodeName); e.state != stateError { |
return e |
} |
} |
|
return context{ |
state: stateError, |
err: errorf(ErrBranchEnd, line, "{{%s}} branches end in different contexts: %v, %v", nodeName, a, b), |
} |
} |
|
// escapeBranch escapes a branch template node: "if", "range" and "with". |
func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context { |
c0 := e.escapeList(c, n.List) |
if nodeName == "range" && c0.state != stateError { |
// The "true" branch of a "range" node can execute multiple times. |
// We check that executing n.List once results in the same context |
// as executing n.List twice. |
c1, _ := e.escapeListConditionally(c0, n.List, nil) |
c0 = join(c0, c1, n.Line, nodeName) |
if c0.state == stateError { |
// Make clear that this is a problem on loop re-entry |
// since developers tend to overlook that branch when |
// debugging templates. |
c0.err.Line = n.Line |
c0.err.Description = "on range loop re-entry: " + c0.err.Description |
return c0 |
} |
} |
c1 := e.escapeList(c, n.ElseList) |
return join(c0, c1, n.Line, nodeName) |
} |
|
// escapeList escapes a list template node. |
func (e *escaper) escapeList(c context, n *parse.ListNode) context { |
if n == nil { |
return c |
} |
for _, m := range n.Nodes { |
c = e.escape(c, m) |
} |
return c |
} |
|
// escapeListConditionally escapes a list node but only preserves edits and |
// inferences in e if the inferences and output context satisfy filter. |
// It returns the best guess at an output context, and the result of the filter |
// which is the same as whether e was updated. |
func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) { |
e1 := newEscaper(e.tmpl) |
// Make type inferences available to f. |
for k, v := range e.output { |
e1.output[k] = v |
} |
c = e1.escapeList(c, n) |
ok := filter != nil && filter(e1, c) |
if ok { |
// Copy inferences and edits from e1 back into e. |
for k, v := range e1.output { |
e.output[k] = v |
} |
for k, v := range e1.derived { |
e.derived[k] = v |
} |
for k, v := range e1.called { |
e.called[k] = v |
} |
for k, v := range e1.actionNodeEdits { |
e.editActionNode(k, v) |
} |
for k, v := range e1.templateNodeEdits { |
e.editTemplateNode(k, v) |
} |
for k, v := range e1.textNodeEdits { |
e.editTextNode(k, v) |
} |
} |
return c, ok |
} |
|
// escapeTemplate escapes a {{template}} call node. |
func (e *escaper) escapeTemplate(c context, n *parse.TemplateNode) context { |
c, name := e.escapeTree(c, n.Name, n.Line) |
if name != n.Name { |
e.editTemplateNode(n, name) |
} |
return c |
} |
|
// escapeTree escapes the named template starting in the given context as |
// necessary and returns its output context. |
func (e *escaper) escapeTree(c context, name string, line int) (context, string) { |
// Mangle the template name with the input context to produce a reliable |
// identifier. |
dname := c.mangle(name) |
e.called[dname] = true |
if out, ok := e.output[dname]; ok { |
// Already escaped. |
return out, dname |
} |
t := e.template(name) |
if t == nil { |
// Two cases: The template exists but is empty, or has never been mentioned at |
// all. Distinguish the cases in the error messages. |
if e.tmpl.set[name] != nil { |
return context{ |
state: stateError, |
err: errorf(ErrNoSuchTemplate, line, "%q is an incomplete or empty template", name), |
}, dname |
} |
return context{ |
state: stateError, |
err: errorf(ErrNoSuchTemplate, line, "no such template %q", name), |
}, dname |
} |
if dname != name { |
// Use any template derived during an earlier call to escapeTemplate |
// with different top level templates, or clone if necessary. |
dt := e.template(dname) |
if dt == nil { |
dt = template.New(dname) |
dt.Tree = &parse.Tree{Name: dname, Root: cloneList(t.Root)} |
e.derived[dname] = dt |
} |
t = dt |
} |
return e.computeOutCtx(c, t), dname |
} |
|
// computeOutCtx takes a template and its start context and computes the output |
// context while storing any inferences in e. |
func (e *escaper) computeOutCtx(c context, t *template.Template) context { |
// Propagate context over the body. |
c1, ok := e.escapeTemplateBody(c, t) |
if !ok { |
// Look for a fixed point by assuming c1 as the output context. |
if c2, ok2 := e.escapeTemplateBody(c1, t); ok2 { |
c1, ok = c2, true |
} |
// Use c1 as the error context if neither assumption worked. |
} |
if !ok && c1.state != stateError { |
return context{ |
state: stateError, |
// TODO: Find the first node with a line in t.text.Tree.Root |
err: errorf(ErrOutputContext, 0, "cannot compute output context for template %s", t.Name()), |
} |
} |
return c1 |
} |
|
// escapeTemplateBody escapes the given template assuming the given output |
// context, and returns the best guess at the output context and whether the |
// assumption was correct. |
func (e *escaper) escapeTemplateBody(c context, t *template.Template) (context, bool) { |
filter := func(e1 *escaper, c1 context) bool { |
if c1.state == stateError { |
// Do not update the input escaper, e. |
return false |
} |
if !e1.called[t.Name()] { |
// If t is not recursively called, then c1 is an |
// accurate output context. |
return true |
} |
// c1 is accurate if it matches our assumed output context. |
return c.eq(c1) |
} |
// We need to assume an output context so that recursive template calls |
// take the fast path out of escapeTree instead of infinitely recursing. |
// Naively assuming that the input context is the same as the output |
// works >90% of the time. |
e.output[t.Name()] = c |
return e.escapeListConditionally(c, t.Tree.Root, filter) |
} |
|
// delimEnds maps each delim to a string of characters that terminate it. |
var delimEnds = [...]string{ |
delimDoubleQuote: `"`, |
delimSingleQuote: "'", |
// Determined empirically by running the below in various browsers. |
// var div = document.createElement("DIV"); |
// for (var i = 0; i < 0x10000; ++i) { |
// div.innerHTML = "<span title=x" + String.fromCharCode(i) + "-bar>"; |
// if (div.getElementsByTagName("SPAN")[0].title.indexOf("bar") < 0) |
// document.write("<p>U+" + i.toString(16)); |
// } |
delimSpaceOrTagEnd: " \t\n\f\r>", |
} |
|
var doctypeBytes = []byte("<!DOCTYPE") |
|
// escapeText escapes a text template node. |
func (e *escaper) escapeText(c context, n *parse.TextNode) context { |
s, written, i, b := n.Text, 0, 0, new(bytes.Buffer) |
for i != len(s) { |
c1, nread := contextAfterText(c, s[i:]) |
i1 := i + nread |
if c.state == stateText || c.state == stateRCDATA { |
end := i1 |
if c1.state != c.state { |
for j := end - 1; j >= i; j-- { |
if s[j] == '<' { |
end = j |
break |
} |
} |
} |
for j := i; j < end; j++ { |
if s[j] == '<' && !bytes.HasPrefix(s[j:], doctypeBytes) { |
b.Write(s[written:j]) |
b.WriteString("<") |
written = j + 1 |
} |
} |
} else if isComment(c.state) && c.delim == delimNone { |
switch c.state { |
case stateJSBlockCmt: |
// http://es5.github.com/#x7.4: |
// "Comments behave like white space and are |
// discarded except that, if a MultiLineComment |
// contains a line terminator character, then |
// the entire comment is considered to be a |
// LineTerminator for purposes of parsing by |
// the syntactic grammar." |
if bytes.IndexAny(s[written:i1], "\n\r\u2028\u2029") != -1 { |
b.WriteByte('\n') |
} else { |
b.WriteByte(' ') |
} |
case stateCSSBlockCmt: |
b.WriteByte(' ') |
} |
written = i1 |
} |
if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone { |
// Preserve the portion between written and the comment start. |
cs := i1 - 2 |
if c1.state == stateHTMLCmt { |
// "<!--" instead of "/*" or "//" |
cs -= 2 |
} |
b.Write(s[written:cs]) |
written = i1 |
} |
if i == i1 && c.state == c1.state { |
panic(fmt.Sprintf("infinite loop from %v to %v on %q..%q", c, c1, s[:i], s[i:])) |
} |
c, i = c1, i1 |
} |
|
if written != 0 && c.state != stateError { |
if !isComment(c.state) || c.delim != delimNone { |
b.Write(n.Text[written:]) |
} |
e.editTextNode(n, b.Bytes()) |
} |
return c |
} |
|
// contextAfterText starts in context c, consumes some tokens from the front of |
// s, then returns the context after those tokens and the unprocessed suffix. |
func contextAfterText(c context, s []byte) (context, int) { |
if c.delim == delimNone { |
c1, i := tSpecialTagEnd(c, s) |
if i == 0 { |
// A special end tag (`</script>`) has been seen and |
// all content preceding it has been consumed. |
return c1, 0 |
} |
// Consider all content up to any end tag. |
return transitionFunc[c.state](c, s[:i]) |
} |
|
i := bytes.IndexAny(s, delimEnds[c.delim]) |
if i == -1 { |
i = len(s) |
} |
if c.delim == delimSpaceOrTagEnd { |
// http://www.w3.org/TR/html5/tokenization.html#attribute-value-unquoted-state |
// lists the runes below as error characters. |
// Error out because HTML parsers may differ on whether |
// "<a id= onclick=f(" ends inside id's or onclick's value, |
// "<a class=`foo " ends inside a value, |
// "<a style=font:'Arial'" needs open-quote fixup. |
// IE treats '`' as a quotation character. |
if j := bytes.IndexAny(s[:i], "\"'<=`"); j >= 0 { |
return context{ |
state: stateError, |
err: errorf(ErrBadHTML, 0, "%q in unquoted attr: %q", s[j:j+1], s[:i]), |
}, len(s) |
} |
} |
if i == len(s) { |
// Remain inside the attribute. |
// Decode the value so non-HTML rules can easily handle |
// <button onclick="alert("Hi!")"> |
// without having to entity decode token boundaries. |
for u := []byte(html.UnescapeString(string(s))); len(u) != 0; { |
c1, i1 := transitionFunc[c.state](c, u) |
c, u = c1, u[i1:] |
} |
return c, len(s) |
} |
if c.delim != delimSpaceOrTagEnd { |
// Consume any quote. |
i++ |
} |
// On exiting an attribute, we discard all state information |
// except the state and element. |
return context{state: stateTag, element: c.element}, i |
} |
|
// editActionNode records a change to an action pipeline for later commit. |
func (e *escaper) editActionNode(n *parse.ActionNode, cmds []string) { |
if _, ok := e.actionNodeEdits[n]; ok { |
panic(fmt.Sprintf("node %s shared between templates", n)) |
} |
e.actionNodeEdits[n] = cmds |
} |
|
// editTemplateNode records a change to a {{template}} callee for later commit. |
func (e *escaper) editTemplateNode(n *parse.TemplateNode, callee string) { |
if _, ok := e.templateNodeEdits[n]; ok { |
panic(fmt.Sprintf("node %s shared between templates", n)) |
} |
e.templateNodeEdits[n] = callee |
} |
|
// editTextNode records a change to a text node for later commit. |
func (e *escaper) editTextNode(n *parse.TextNode, text []byte) { |
if _, ok := e.textNodeEdits[n]; ok { |
panic(fmt.Sprintf("node %s shared between templates", n)) |
} |
e.textNodeEdits[n] = text |
} |
|
// commit applies changes to actions and template calls needed to contextually |
// autoescape content and adds any derived templates to the set. |
func (e *escaper) commit() { |
for name := range e.output { |
e.template(name).Funcs(funcMap) |
} |
for _, t := range e.derived { |
if _, err := e.tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil { |
panic("error adding derived template") |
} |
} |
for n, s := range e.actionNodeEdits { |
ensurePipelineContains(n.Pipe, s) |
} |
for n, name := range e.templateNodeEdits { |
n.Name = name |
} |
for n, s := range e.textNodeEdits { |
n.Text = s |
} |
} |
|
// template returns the named template given a mangled template name. |
func (e *escaper) template(name string) *template.Template { |
t := e.tmpl.text.Lookup(name) |
if t == nil { |
t = e.derived[name] |
} |
return t |
} |
/template/clone.go
0,0 → 1,90
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"text/template/parse" |
) |
|
// clone clones a template Node. |
func clone(n parse.Node) parse.Node { |
switch t := n.(type) { |
case *parse.ActionNode: |
return cloneAction(t) |
case *parse.IfNode: |
b := new(parse.IfNode) |
copyBranch(&b.BranchNode, &t.BranchNode) |
return b |
case *parse.ListNode: |
return cloneList(t) |
case *parse.RangeNode: |
b := new(parse.RangeNode) |
copyBranch(&b.BranchNode, &t.BranchNode) |
return b |
case *parse.TemplateNode: |
return cloneTemplate(t) |
case *parse.TextNode: |
return cloneText(t) |
case *parse.WithNode: |
b := new(parse.WithNode) |
copyBranch(&b.BranchNode, &t.BranchNode) |
return b |
} |
panic("cloning " + n.String() + " is unimplemented") |
} |
|
// cloneAction returns a deep clone of n. |
func cloneAction(n *parse.ActionNode) *parse.ActionNode { |
// We use keyless fields because they won't compile if a field is added. |
return &parse.ActionNode{n.NodeType, n.Line, clonePipe(n.Pipe)} |
} |
|
// cloneList returns a deep clone of n. |
func cloneList(n *parse.ListNode) *parse.ListNode { |
if n == nil { |
return nil |
} |
// We use keyless fields because they won't compile if a field is added. |
c := parse.ListNode{n.NodeType, make([]parse.Node, len(n.Nodes))} |
for i, child := range n.Nodes { |
c.Nodes[i] = clone(child) |
} |
return &c |
} |
|
// clonePipe returns a shallow clone of n. |
// The escaper does not modify pipe descendants in place so there's no need to |
// clone deeply. |
func clonePipe(n *parse.PipeNode) *parse.PipeNode { |
if n == nil { |
return nil |
} |
// We use keyless fields because they won't compile if a field is added. |
return &parse.PipeNode{n.NodeType, n.Line, n.Decl, n.Cmds} |
} |
|
// cloneTemplate returns a deep clone of n. |
func cloneTemplate(n *parse.TemplateNode) *parse.TemplateNode { |
// We use keyless fields because they won't compile if a field is added. |
return &parse.TemplateNode{n.NodeType, n.Line, n.Name, clonePipe(n.Pipe)} |
} |
|
// cloneText clones the given node sharing its []byte. |
func cloneText(n *parse.TextNode) *parse.TextNode { |
// We use keyless fields because they won't compile if a field is added. |
return &parse.TextNode{n.NodeType, n.Text} |
} |
|
// copyBranch clones src into dst. |
func copyBranch(dst, src *parse.BranchNode) { |
// We use keyless fields because they won't compile if a field is added. |
*dst = parse.BranchNode{ |
src.NodeType, |
src.Line, |
clonePipe(src.Pipe), |
cloneList(src.List), |
cloneList(src.ElseList), |
} |
} |
/template/url_test.go
0,0 → 1,112
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"testing" |
) |
|
func TestURLNormalizer(t *testing.T) { |
tests := []struct { |
url, want string |
}{ |
{"", ""}, |
{ |
"http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag", |
"http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag", |
}, |
{" ", "%20"}, |
{"%7c", "%7c"}, |
{"%7C", "%7C"}, |
{"%2", "%252"}, |
{"%", "%25"}, |
{"%z", "%25z"}, |
{"/foo|bar/%5c\u1234", "/foo%7cbar/%5c%e1%88%b4"}, |
} |
for _, test := range tests { |
if got := urlNormalizer(test.url); test.want != got { |
t.Errorf("%q: want\n\t%q\nbut got\n\t%q", test.url, test.want, got) |
} |
if test.want != urlNormalizer(test.want) { |
t.Errorf("not idempotent: %q", test.want) |
} |
} |
} |
|
func TestURLFilters(t *testing.T) { |
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") |
|
tests := []struct { |
name string |
escaper func(...interface{}) string |
escaped string |
}{ |
{ |
"urlEscaper", |
urlEscaper, |
"%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" + |
"%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" + |
"%20%21%22%23%24%25%26%27%28%29%2a%2b%2c-.%2f" + |
"0123456789%3a%3b%3c%3d%3e%3f" + |
"%40ABCDEFGHIJKLMNO" + |
"PQRSTUVWXYZ%5b%5c%5d%5e_" + |
"%60abcdefghijklmno" + |
"pqrstuvwxyz%7b%7c%7d~%7f" + |
"%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e", |
}, |
{ |
"urlNormalizer", |
urlNormalizer, |
"%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" + |
"%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" + |
"%20!%22#$%25&%27%28%29*+,-./" + |
"0123456789:;%3c=%3e?" + |
"@ABCDEFGHIJKLMNO" + |
"PQRSTUVWXYZ[%5c]%5e_" + |
"%60abcdefghijklmno" + |
"pqrstuvwxyz%7b%7c%7d~%7f" + |
"%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e", |
}, |
} |
|
for _, test := range tests { |
if s := test.escaper(input); s != test.escaped { |
t.Errorf("%s: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) |
continue |
} |
} |
} |
|
func BenchmarkURLEscaper(b *testing.B) { |
for i := 0; i < b.N; i++ { |
urlEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag") |
} |
} |
|
func BenchmarkURLEscaperNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
urlEscaper("TheQuickBrownFoxJumpsOverTheLazyDog.") |
} |
} |
|
func BenchmarkURLNormalizer(b *testing.B) { |
for i := 0; i < b.N; i++ { |
urlNormalizer("The quick brown fox jumps over the lazy dog.\n") |
} |
} |
|
func BenchmarkURLNormalizerNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
urlNormalizer("http://example.com:80/foo?q=bar%20&baz=x+y#frag") |
} |
} |
/template/url.go
0,0 → 1,105
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"fmt" |
"strings" |
) |
|
// urlFilter returns its input unless it contains an unsafe protocol in which |
// case it defangs the entire URL. |
func urlFilter(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeURL { |
return s |
} |
if i := strings.IndexRune(s, ':'); i >= 0 && strings.IndexRune(s[:i], '/') < 0 { |
protocol := strings.ToLower(s[:i]) |
if protocol != "http" && protocol != "https" && protocol != "mailto" { |
return "#" + filterFailsafe |
} |
} |
return s |
} |
|
// urlEscaper produces an output that can be embedded in a URL query. |
// The output can be embedded in an HTML attribute without further escaping. |
func urlEscaper(args ...interface{}) string { |
return urlProcessor(false, args...) |
} |
|
// urlEscaper normalizes URL content so it can be embedded in a quote-delimited |
// string or parenthesis delimited url(...). |
// The normalizer does not encode all HTML specials. Specifically, it does not |
// encode '&' so correct embedding in an HTML attribute requires escaping of |
// '&' to '&'. |
func urlNormalizer(args ...interface{}) string { |
return urlProcessor(true, args...) |
} |
|
// urlProcessor normalizes (when norm is true) or escapes its input to produce |
// a valid hierarchical or opaque URL part. |
func urlProcessor(norm bool, args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeURL { |
norm = true |
} |
var b bytes.Buffer |
written := 0 |
// The byte loop below assumes that all URLs use UTF-8 as the |
// content-encoding. This is similar to the URI to IRI encoding scheme |
// defined in section 3.1 of RFC 3987, and behaves the same as the |
// EcmaScript builtin encodeURIComponent. |
// It should not cause any misencoding of URLs in pages with |
// Content-type: text/html;charset=UTF-8. |
for i, n := 0, len(s); i < n; i++ { |
c := s[i] |
switch c { |
// Single quote and parens are sub-delims in RFC 3986, but we |
// escape them so the output can be embedded in in single |
// quoted attributes and unquoted CSS url(...) constructs. |
// Single quotes are reserved in URLs, but are only used in |
// the obsolete "mark" rule in an appendix in RFC 3986 |
// so can be safely encoded. |
case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']': |
if norm { |
continue |
} |
// Unreserved according to RFC 3986 sec 2.3 |
// "For consistency, percent-encoded octets in the ranges of |
// ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), |
// period (%2E), underscore (%5F), or tilde (%7E) should not be |
// created by URI producers |
case '-', '.', '_', '~': |
continue |
case '%': |
// When normalizing do not re-encode valid escapes. |
if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) { |
continue |
} |
default: |
// Unreserved according to RFC 3986 sec 2.3 |
if 'a' <= c && c <= 'z' { |
continue |
} |
if 'A' <= c && c <= 'Z' { |
continue |
} |
if '0' <= c && c <= '9' { |
continue |
} |
} |
b.WriteString(s[written:i]) |
fmt.Fprintf(&b, "%%%02x", c) |
written = i + 1 |
} |
if written == 0 { |
return s |
} |
b.WriteString(s[written:]) |
return b.String() |
} |
/template/html_test.go
0,0 → 1,94
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"html" |
"strings" |
"testing" |
) |
|
func TestHTMLNospaceEscaper(t *testing.T) { |
input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
"`abcdefghijklmno" + |
"pqrstuvwxyz{|}~\x7f" + |
"\u00A0\u0100\u2028\u2029\ufeff\ufdec\U0001D11E") |
|
want := ("�\x01\x02\x03\x04\x05\x06\x07" + |
"\x08	  \x0E\x0F" + |
"\x10\x11\x12\x13\x14\x15\x16\x17" + |
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
` !"#$%&'()*+,-./` + |
`0123456789:;<=>?` + |
`@ABCDEFGHIJKLMNO` + |
`PQRSTUVWXYZ[\]^_` + |
``abcdefghijklmno` + |
`pqrstuvwxyz{|}~` + "\u007f" + |
"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") |
|
got := htmlNospaceEscaper(input) |
if got != want { |
t.Errorf("encode: want\n\t%q\nbut got\n\t%q", want, got) |
} |
|
got, want = html.UnescapeString(got), strings.Replace(input, "\x00", "\ufffd", 1) |
if want != got { |
t.Errorf("decode: want\n\t%q\nbut got\n\t%q", want, got) |
} |
} |
|
func TestStripTags(t *testing.T) { |
tests := []struct { |
input, want string |
}{ |
{"", ""}, |
{"Hello, World!", "Hello, World!"}, |
{"foo&bar", "foo&bar"}, |
{`Hello <a href="www.example.com/">World</a>!`, "Hello World!"}, |
{"Foo <textarea>Bar</textarea> Baz", "Foo Bar Baz"}, |
{"Foo <!-- Bar --> Baz", "Foo Baz"}, |
{"<", "<"}, |
{"foo < bar", "foo < bar"}, |
{`Foo<script type="text/javascript">alert(1337)</script>Bar`, "FooBar"}, |
{`Foo<div title="1>2">Bar`, "FooBar"}, |
{`I <3 Ponies!`, `I <3 Ponies!`}, |
{`<script>foo()</script>`, ``}, |
} |
|
for _, test := range tests { |
if got := stripTags(test.input); got != test.want { |
t.Errorf("%q: want %q, got %q", test.input, test.want, got) |
} |
} |
} |
|
func BenchmarkHTMLNospaceEscaper(b *testing.B) { |
for i := 0; i < b.N; i++ { |
htmlNospaceEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
|
func BenchmarkHTMLNospaceEscaperNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
htmlNospaceEscaper("The_quick,_brown_fox_jumps_over_the_lazy_dog.") |
} |
} |
|
func BenchmarkStripTags(b *testing.B) { |
for i := 0; i < b.N; i++ { |
stripTags("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") |
} |
} |
|
func BenchmarkStripTagsNoSpecials(b *testing.B) { |
for i := 0; i < b.N; i++ { |
stripTags("The quick, brown fox jumps over the lazy dog.") |
} |
} |
/template/html.go
0,0 → 1,257
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"fmt" |
"strings" |
"unicode/utf8" |
) |
|
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values. |
func htmlNospaceEscaper(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeHTML { |
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false) |
} |
return htmlReplacer(s, htmlNospaceReplacementTable, false) |
} |
|
// attrEscaper escapes for inclusion in quoted attribute values. |
func attrEscaper(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeHTML { |
return htmlReplacer(stripTags(s), htmlNormReplacementTable, true) |
} |
return htmlReplacer(s, htmlReplacementTable, true) |
} |
|
// rcdataEscaper escapes for inclusion in an RCDATA element body. |
func rcdataEscaper(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeHTML { |
return htmlReplacer(s, htmlNormReplacementTable, true) |
} |
return htmlReplacer(s, htmlReplacementTable, true) |
} |
|
// htmlEscaper escapes for inclusion in HTML text. |
func htmlEscaper(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeHTML { |
return s |
} |
return htmlReplacer(s, htmlReplacementTable, true) |
} |
|
// htmlReplacementTable contains the runes that need to be escaped |
// inside a quoted attribute value or in a text node. |
var htmlReplacementTable = []string{ |
// http://www.w3.org/TR/html5/tokenization.html#attribute-value-unquoted-state: " |
// U+0000 NULL Parse error. Append a U+FFFD REPLACEMENT |
// CHARACTER character to the current attribute's value. |
// " |
// and similarly |
// http://www.w3.org/TR/html5/tokenization.html#before-attribute-value-state |
0: "\uFFFD", |
'"': """, |
'&': "&", |
'\'': "'", |
'+': "+", |
'<': "<", |
'>': ">", |
} |
|
// htmlNormReplacementTable is like htmlReplacementTable but without '&' to |
// avoid over-encoding existing entities. |
var htmlNormReplacementTable = []string{ |
0: "\uFFFD", |
'"': """, |
'\'': "'", |
'+': "+", |
'<': "<", |
'>': ">", |
} |
|
// htmlNospaceReplacementTable contains the runes that need to be escaped |
// inside an unquoted attribute value. |
// The set of runes escaped is the union of the HTML specials and |
// those determined by running the JS below in browsers: |
// <div id=d></div> |
// <script>(function () { |
// var a = [], d = document.getElementById("d"), i, c, s; |
// for (i = 0; i < 0x10000; ++i) { |
// c = String.fromCharCode(i); |
// d.innerHTML = "<span title=" + c + "lt" + c + "></span>" |
// s = d.getElementsByTagName("SPAN")[0]; |
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); } |
// } |
// document.write(a.join(", ")); |
// })()</script> |
var htmlNospaceReplacementTable = []string{ |
0: "�", |
'\t': "	", |
'\n': " ", |
'\v': "", |
'\f': "", |
'\r': " ", |
' ': " ", |
'"': """, |
'&': "&", |
'\'': "'", |
'+': "+", |
'<': "<", |
'=': "=", |
'>': ">", |
// A parse error in the attribute value (unquoted) and |
// before attribute value states. |
// Treated as a quoting character by IE. |
'`': "`", |
} |
|
// htmlNospaceNormReplacementTable is like htmlNospaceReplacementTable but |
// without '&' to avoid over-encoding existing entities. |
var htmlNospaceNormReplacementTable = []string{ |
0: "�", |
'\t': "	", |
'\n': " ", |
'\v': "", |
'\f': "", |
'\r': " ", |
' ': " ", |
'"': """, |
'\'': "'", |
'+': "+", |
'<': "<", |
'=': "=", |
'>': ">", |
// A parse error in the attribute value (unquoted) and |
// before attribute value states. |
// Treated as a quoting character by IE. |
'`': "`", |
} |
|
// htmlReplacer returns s with runes replaced acccording to replacementTable |
// and when badRunes is true, certain bad runes are allowed through unescaped. |
func htmlReplacer(s string, replacementTable []string, badRunes bool) string { |
written, b := 0, new(bytes.Buffer) |
for i, r := range s { |
if int(r) < len(replacementTable) { |
if repl := replacementTable[r]; len(repl) != 0 { |
b.WriteString(s[written:i]) |
b.WriteString(repl) |
// Valid as long as replacementTable doesn't |
// include anything above 0x7f. |
written = i + utf8.RuneLen(r) |
} |
} else if badRunes { |
// No-op. |
// IE does not allow these ranges in unquoted attrs. |
} else if 0xfdd0 <= r && r <= 0xfdef || 0xfff0 <= r && r <= 0xffff { |
fmt.Fprintf(b, "%s&#x%x;", s[written:i], r) |
written = i + utf8.RuneLen(r) |
} |
} |
if written == 0 { |
return s |
} |
b.WriteString(s[written:]) |
return b.String() |
} |
|
// stripTags takes a snippet of HTML and returns only the text content. |
// For example, `<b>¡Hi!</b> <script>...</script>` -> `¡Hi! `. |
func stripTags(html string) string { |
var b bytes.Buffer |
s, c, i, allText := []byte(html), context{}, 0, true |
// Using the transition funcs helps us avoid mangling |
// `<div title="1>2">` or `I <3 Ponies!`. |
for i != len(s) { |
if c.delim == delimNone { |
st := c.state |
// Use RCDATA instead of parsing into JS or CSS styles. |
if c.element != elementNone && !isInTag(st) { |
st = stateRCDATA |
} |
d, nread := transitionFunc[st](c, s[i:]) |
i1 := i + nread |
if c.state == stateText || c.state == stateRCDATA { |
// Emit text up to the start of the tag or comment. |
j := i1 |
if d.state != c.state { |
for j1 := j - 1; j1 >= i; j1-- { |
if s[j1] == '<' { |
j = j1 |
break |
} |
} |
} |
b.Write(s[i:j]) |
} else { |
allText = false |
} |
c, i = d, i1 |
continue |
} |
i1 := i + bytes.IndexAny(s[i:], delimEnds[c.delim]) |
if i1 < i { |
break |
} |
if c.delim != delimSpaceOrTagEnd { |
// Consume any quote. |
i1++ |
} |
c, i = context{state: stateTag, element: c.element}, i1 |
} |
if allText { |
return html |
} else if c.state == stateText || c.state == stateRCDATA { |
b.Write(s[i:]) |
} |
return b.String() |
} |
|
// htmlNameFilter accepts valid parts of an HTML attribute or tag name or |
// a known-safe HTML attribute. |
func htmlNameFilter(args ...interface{}) string { |
s, t := stringify(args...) |
if t == contentTypeHTMLAttr { |
return s |
} |
if len(s) == 0 { |
// Avoid violation of structure preservation. |
// <input checked {{.K}}={{.V}}>. |
// Without this, if .K is empty then .V is the value of |
// checked, but otherwise .V is the value of the attribute |
// named .K. |
return filterFailsafe |
} |
s = strings.ToLower(s) |
if t := attrType(s); t != contentTypePlain { |
// TODO: Split attr and element name part filters so we can whitelist |
// attributes. |
return filterFailsafe |
} |
for _, r := range s { |
switch { |
case '0' <= r && r <= '9': |
case 'a' <= r && r <= 'z': |
default: |
return filterFailsafe |
} |
} |
return s |
} |
|
// commentEscaper returns the empty string regardless of input. |
// Comment content does not correspond to any parsed structure or |
// human-readable content, so the simplest and most secure policy is to drop |
// content interpolated into comments. |
// This approach is equally valid whether or not static comment content is |
// removed from the template. |
func commentEscaper(args ...interface{}) string { |
return "" |
} |
/template/doc.go
0,0 → 1,186
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
/* |
Package template (html/template) is a specialization of package text/template |
that automates the construction of HTML output that is safe against code |
injection. |
|
|
Introduction |
|
This package wraps package template so you can use the standard template API |
to parse and execute templates. |
|
set, err := new(template.Set).Parse(...) |
// Error checking elided |
err = set.Execute(out, "Foo", data) |
|
If successful, set will now be injection-safe. Otherwise, err is an error |
defined in the docs for ErrorCode. |
|
HTML templates treat data values as plain text which should be encoded so they |
can be safely embedded in an HTML document. The escaping is contextual, so |
actions can appear within JavaScript, CSS, and URI contexts. |
|
The security model used by this package assumes that template authors are |
trusted, while Execute's data parameter is not. More details are provided below. |
|
Example |
|
import "text/template" |
... |
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) |
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") |
|
produces |
|
Hello, <script>alert('you have been pwned')</script>! |
|
but with contextual autoescaping, |
|
import "html/template" |
... |
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) |
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") |
|
produces safe, escaped HTML output |
|
Hello, <script>alert('you have been pwned')</script>! |
|
|
Contexts |
|
This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing |
functions to each simple action pipeline, so given the excerpt |
|
<a href="/search?q={{.}}">{{.}}</a> |
|
At parse time each {{.}} is overwritten to add escaping functions as necessary. |
In this case it becomes |
|
<a href="/search?q={{. | urlquery}}">{{. | html}}</a> |
|
|
Errors |
|
See the documentation of ErrorCode for details. |
|
|
A fuller picture |
|
The rest of this package comment may be skipped on first reading; it includes |
details necessary to understand escaping contexts and error messages. Most users |
will not need to understand these details. |
|
|
Contexts |
|
Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows |
how {{.}} appears when used in the context to the left. |
|
Context {{.}} After |
{{.}} O'Reilly: How are <i>you</i>? |
<a title='{{.}}'> O'Reilly: How are you? |
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e? |
<a href="?q={{.}}"> O'Reilly%3a%20How%20are%3ci%3e...%3f |
<a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...? |
<a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?" |
<a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f |
|
If used in an unsafe context, then the value might be filtered out: |
|
Context {{.}} After |
<a href="{{.}}"> #ZgotmplZ |
|
since "O'Reilly:" is not an allowed protocol like "http:". |
|
|
If {{.}} is the innocuous word, `left`, then it can appear more widely, |
|
Context {{.}} After |
{{.}} left |
<a title='{{.}}'> left |
<a href='{{.}}'> left |
<a href='/{{.}}'> left |
<a href='?dir={{.}}'> left |
<a style="border-{{.}}: 4px"> left |
<a style="align: {{.}}"> left |
<a style="background: '{{.}}'> left |
<a style="background: url('{{.}}')> left |
<style>p.{{.}} {color:red}</style> left |
|
Non-string values can be used in JavaScript contexts. |
If {{.}} is |
|
[]struct{A,B string}{ "foo", "bar" } |
|
in the escaped template |
|
<script>var pair = {{.}};</script> |
|
then the template output is |
|
<script>var pair = {"A": "foo", "B": "bar"};</script> |
|
See package json to understand how non-string content is marshalled for |
embedding in JavaScript contexts. |
|
|
Typed Strings |
|
By default, this package assumes that all pipelines produce a plain text string. |
It adds escaping pipeline stages necessary to correctly and safely embed that |
plain text string in the appropriate context. |
|
When a data value is not plain text, you can make sure it is not over-escaped |
by marking it with its type. |
|
Types HTML, JS, URL, and others from content.go can carry safe content that is |
exempted from escaping. |
|
The template |
|
Hello, {{.}}! |
|
can be invoked with |
|
tmpl.Execute(out, HTML(`<b>World</b>`)) |
|
to produce |
|
Hello, <b>World</b>! |
|
instead of the |
|
Hello, <b>World<b>! |
|
that would have been produced if {{.}} was a regular string. |
|
|
Security Model |
|
http://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#problem_definition defines "safe" as used by this package. |
|
This package assumes that template authors are trusted, that Execute's data |
parameter is not, and seeks to preserve the properties below in the face |
of untrusted data: |
|
Structure Preservation Property |
"... when a template author writes an HTML tag in a safe templating language, |
the browser will interpret the corresponding portion of the output as a tag |
regardless of the values of untrusted data, and similarly for other structures |
such as attribute boundaries and JS and CSS string boundaries." |
|
Code Effect Property |
"... only code specified by the template author should run as a result of |
injecting the template output into a page and all code specified by the |
template author should run as a result of the same." |
|
Least Surprise Property |
"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who |
knows that contextual autoescaping happens should be able to look at a {{.}} |
and correctly infer what sanitization happens." |
*/ |
package template |
/template/content_test.go
0,0 → 1,221
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"bytes" |
"strings" |
"testing" |
) |
|
func TestTypedContent(t *testing.T) { |
data := []interface{}{ |
`<b> "foo%" O'Reilly &bar;`, |
CSS(`a[href =~ "//example.com"]#foo`), |
HTML(`Hello, <b>World</b> &tc!`), |
HTMLAttr(` dir="ltr"`), |
JS(`c && alert("Hello, World!");`), |
JSStr(`Hello, World & O'Reilly\x21`), |
URL(`greeting=H%69&addressee=(World)`), |
} |
|
// For each content sensitive escaper, see how it does on |
// each of the typed strings above. |
tests := []struct { |
// A template containing a single {{.}}. |
input string |
want []string |
}{ |
{ |
`<style>{{.}} { color: blue }</style>`, |
[]string{ |
`ZgotmplZ`, |
// Allowed but not escaped. |
`a[href =~ "//example.com"]#foo`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
}, |
}, |
{ |
`<div style="{{.}}">`, |
[]string{ |
`ZgotmplZ`, |
// Allowed and HTML escaped. |
`a[href =~ "//example.com"]#foo`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
}, |
}, |
{ |
`{{.}}`, |
[]string{ |
`<b> "foo%" O'Reilly &bar;`, |
`a[href =~ "//example.com"]#foo`, |
// Not escaped. |
`Hello, <b>World</b> &tc!`, |
` dir="ltr"`, |
`c && alert("Hello, World!");`, |
`Hello, World & O'Reilly\x21`, |
`greeting=H%69&addressee=(World)`, |
}, |
}, |
{ |
`<a{{.}}>`, |
[]string{ |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
// Allowed and HTML escaped. |
` dir="ltr"`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
`ZgotmplZ`, |
}, |
}, |
{ |
`<a title={{.}}>`, |
[]string{ |
`<b> "foo%" O'Reilly &bar;`, |
`a[href =~ "//example.com"]#foo`, |
// Tags stripped, spaces escaped, entity not re-escaped. |
`Hello, World &tc!`, |
` dir="ltr"`, |
`c && alert("Hello, World!");`, |
`Hello, World & O'Reilly\x21`, |
`greeting=H%69&addressee=(World)`, |
}, |
}, |
{ |
`<a title='{{.}}'>`, |
[]string{ |
`<b> "foo%" O'Reilly &bar;`, |
`a[href =~ "//example.com"]#foo`, |
// Tags stripped, entity not re-escaped. |
`Hello, World &tc!`, |
` dir="ltr"`, |
`c && alert("Hello, World!");`, |
`Hello, World & O'Reilly\x21`, |
`greeting=H%69&addressee=(World)`, |
}, |
}, |
{ |
`<textarea>{{.}}</textarea>`, |
[]string{ |
`<b> "foo%" O'Reilly &bar;`, |
`a[href =~ "//example.com"]#foo`, |
// Angle brackets escaped to prevent injection of close tags, entity not re-escaped. |
`Hello, <b>World</b> &tc!`, |
` dir="ltr"`, |
`c && alert("Hello, World!");`, |
`Hello, World & O'Reilly\x21`, |
`greeting=H%69&addressee=(World)`, |
}, |
}, |
{ |
`<script>alert({{.}})</script>`, |
[]string{ |
`"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, |
`"a[href =~ \"//example.com\"]#foo"`, |
`"Hello, \u003cb\u003eWorld\u003c/b\u003e &tc!"`, |
`" dir=\"ltr\""`, |
// Not escaped. |
`c && alert("Hello, World!");`, |
// Escape sequence not over-escaped. |
`"Hello, World & O'Reilly\x21"`, |
`"greeting=H%69&addressee=(World)"`, |
}, |
}, |
{ |
`<button onclick="alert({{.}})">`, |
[]string{ |
`"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, |
`"a[href =~ \"//example.com\"]#foo"`, |
`"Hello, \u003cb\u003eWorld\u003c/b\u003e &amp;tc!"`, |
`" dir=\"ltr\""`, |
// Not JS escaped but HTML escaped. |
`c && alert("Hello, World!");`, |
// Escape sequence not over-escaped. |
`"Hello, World & O'Reilly\x21"`, |
`"greeting=H%69&addressee=(World)"`, |
}, |
}, |
{ |
`<script>alert("{{.}}")</script>`, |
[]string{ |
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`, |
`a[href =~ \x22\/\/example.com\x22]#foo`, |
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`, |
` dir=\x22ltr\x22`, |
`c \x26\x26 alert(\x22Hello, World!\x22);`, |
// Escape sequence not over-escaped. |
`Hello, World \x26 O\x27Reilly\x21`, |
`greeting=H%69\x26addressee=(World)`, |
}, |
}, |
{ |
`<button onclick='alert("{{.}}")'>`, |
[]string{ |
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`, |
`a[href =~ \x22\/\/example.com\x22]#foo`, |
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`, |
` dir=\x22ltr\x22`, |
`c \x26\x26 alert(\x22Hello, World!\x22);`, |
// Escape sequence not over-escaped. |
`Hello, World \x26 O\x27Reilly\x21`, |
`greeting=H%69\x26addressee=(World)`, |
}, |
}, |
{ |
`<a href="?q={{.}}">`, |
[]string{ |
`%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`, |
`a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`, |
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`, |
`%20dir%3d%22ltr%22`, |
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`, |
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`, |
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done. |
`greeting=H%69&addressee=%28World%29`, |
}, |
}, |
{ |
`<style>body { background: url('?img={{.}}') }</style>`, |
[]string{ |
`%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`, |
`a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`, |
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`, |
`%20dir%3d%22ltr%22`, |
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`, |
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`, |
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done. |
`greeting=H%69&addressee=%28World%29`, |
}, |
}, |
} |
|
for _, test := range tests { |
tmpl := Must(New("x").Parse(test.input)) |
pre := strings.Index(test.input, "{{.}}") |
post := len(test.input) - (pre + 5) |
var b bytes.Buffer |
for i, x := range data { |
b.Reset() |
if err := tmpl.Execute(&b, x); err != nil { |
t.Errorf("%q with %v: %s", test.input, x, err) |
continue |
} |
if want, got := test.want[i], b.String()[pre:b.Len()-post]; want != got { |
t.Errorf("%q with %v:\nwant\n\t%q,\ngot\n\t%q\n", test.input, x, want, got) |
continue |
} |
} |
} |
} |
/template/content.go
0,0 → 1,113
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"fmt" |
"reflect" |
) |
|
// Strings of content from a trusted source. |
type ( |
// CSS encapsulates known safe content that matches any of: |
// 1. The CSS3 stylesheet production, such as `p { color: purple }`. |
// 2. The CSS3 rule production, such as `a[href=~"https:"].foo#bar`. |
// 3. CSS3 declaration productions, such as `color: red; margin: 2px`. |
// 4. The CSS3 value production, such as `rgba(0, 0, 255, 127)`. |
// See http://www.w3.org/TR/css3-syntax/#style |
CSS string |
|
// HTML encapsulates a known safe HTML document fragment. |
// It should not be used for HTML from a third-party, or HTML with |
// unclosed tags or comments. The outputs of a sound HTML sanitizer |
// and a template escaped by this package are fine for use with HTML. |
HTML string |
|
// HTMLAttr encapsulates an HTML attribute from a trusted source, |
// for example: ` dir="ltr"`. |
HTMLAttr string |
|
// JS encapsulates a known safe EcmaScript5 Expression, or example, |
// `(x + y * z())`. |
// Template authors are responsible for ensuring that typed expressions |
// do not break the intended precedence and that there is no |
// statement/expression ambiguity as when passing an expression like |
// "{ foo: bar() }\n['foo']()", which is both a valid Expression and a |
// valid Program with a very different meaning. |
JS string |
|
// JSStr encapsulates a sequence of characters meant to be embedded |
// between quotes in a JavaScript expression. |
// The string must match a series of StringCharacters: |
// StringCharacter :: SourceCharacter but not `\` or LineTerminator |
// | EscapeSequence |
// Note that LineContinuations are not allowed. |
// JSStr("foo\\nbar") is fine, but JSStr("foo\\\nbar") is not. |
JSStr string |
|
// URL encapsulates a known safe URL as defined in RFC 3896. |
// A URL like `javascript:checkThatFormNotEditedBeforeLeavingPage()` |
// from a trusted source should go in the page, but by default dynamic |
// `javascript:` URLs are filtered out since they are a frequently |
// exploited injection vector. |
URL string |
) |
|
type contentType uint8 |
|
const ( |
contentTypePlain contentType = iota |
contentTypeCSS |
contentTypeHTML |
contentTypeHTMLAttr |
contentTypeJS |
contentTypeJSStr |
contentTypeURL |
// contentTypeUnsafe is used in attr.go for values that affect how |
// embedded content and network messages are formed, vetted, |
// or interpreted; or which credentials network messages carry. |
contentTypeUnsafe |
) |
|
// indirect returns the value, after dereferencing as many times |
// as necessary to reach the base type (or nil). |
func indirect(a interface{}) interface{} { |
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { |
// Avoid creating a reflect.Value if it's not a pointer. |
return a |
} |
v := reflect.ValueOf(a) |
for v.Kind() == reflect.Ptr && !v.IsNil() { |
v = v.Elem() |
} |
return v.Interface() |
} |
|
// stringify converts its arguments to a string and the type of the content. |
// All pointers are dereferenced, as in the text/template package. |
func stringify(args ...interface{}) (string, contentType) { |
if len(args) == 1 { |
switch s := indirect(args[0]).(type) { |
case string: |
return s, contentTypePlain |
case CSS: |
return string(s), contentTypeCSS |
case HTML: |
return string(s), contentTypeHTML |
case HTMLAttr: |
return string(s), contentTypeHTMLAttr |
case JS: |
return string(s), contentTypeJS |
case JSStr: |
return string(s), contentTypeJSStr |
case URL: |
return string(s), contentTypeURL |
} |
} |
for i, arg := range args { |
args[i] = indirect(arg) |
} |
return fmt.Sprint(args...), contentTypePlain |
} |
/template/attr.go
0,0 → 1,175
// Copyright 2011 The Go Authors. All rights reserved. |
// Use of this source code is governed by a BSD-style |
// license that can be found in the LICENSE file. |
|
package template |
|
import ( |
"strings" |
) |
|
// attrTypeMap[n] describes the value of the given attribute. |
// If an attribute affects (or can mask) the encoding or interpretation of |
// other content, or affects the contents, idempotency, or credentials of a |
// network message, then the value in this map is contentTypeUnsafe. |
// This map is derived from HTML5, specifically |
// http://www.w3.org/TR/html5/Overview.html#attributes-1 |
// as well as "%URI"-typed attributes from |
// http://www.w3.org/TR/html4/index/attributes.html |
var attrTypeMap = map[string]contentType{ |
"accept": contentTypePlain, |
"accept-charset": contentTypeUnsafe, |
"action": contentTypeURL, |
"alt": contentTypePlain, |
"archive": contentTypeURL, |
"async": contentTypeUnsafe, |
"autocomplete": contentTypePlain, |
"autofocus": contentTypePlain, |
"autoplay": contentTypePlain, |
"background": contentTypeURL, |
"border": contentTypePlain, |
"checked": contentTypePlain, |
"cite": contentTypeURL, |
"challenge": contentTypeUnsafe, |
"charset": contentTypeUnsafe, |
"class": contentTypePlain, |
"classid": contentTypeURL, |
"codebase": contentTypeURL, |
"cols": contentTypePlain, |
"colspan": contentTypePlain, |
"content": contentTypeUnsafe, |
"contenteditable": contentTypePlain, |
"contextmenu": contentTypePlain, |
"controls": contentTypePlain, |
"coords": contentTypePlain, |
"crossorigin": contentTypeUnsafe, |
"data": contentTypeURL, |
"datetime": contentTypePlain, |
"default": contentTypePlain, |
"defer": contentTypeUnsafe, |
"dir": contentTypePlain, |
"dirname": contentTypePlain, |
"disabled": contentTypePlain, |
"draggable": contentTypePlain, |
"dropzone": contentTypePlain, |
"enctype": contentTypeUnsafe, |
"for": contentTypePlain, |
"form": contentTypeUnsafe, |
"formaction": contentTypeURL, |
"formenctype": contentTypeUnsafe, |
"formmethod": contentTypeUnsafe, |
"formnovalidate": contentTypeUnsafe, |
"formtarget": contentTypePlain, |
"headers": contentTypePlain, |
"height": contentTypePlain, |
"hidden": contentTypePlain, |
"high": contentTypePlain, |
"href": contentTypeURL, |
"hreflang": contentTypePlain, |
"http-equiv": contentTypeUnsafe, |
"icon": contentTypeURL, |
"id": contentTypePlain, |
"ismap": contentTypePlain, |
"keytype": contentTypeUnsafe, |
"kind": contentTypePlain, |
"label": contentTypePlain, |
"lang": contentTypePlain, |
"language": contentTypeUnsafe, |
"list": contentTypePlain, |
"longdesc": contentTypeURL, |
"loop": contentTypePlain, |
"low": contentTypePlain, |
"manifest": contentTypeURL, |
"max": contentTypePlain, |
"maxlength": contentTypePlain, |
"media": contentTypePlain, |
"mediagroup": contentTypePlain, |
"method": contentTypeUnsafe, |
"min": contentTypePlain, |
"multiple": contentTypePlain, |
"name": contentTypePlain, |
"novalidate": contentTypeUnsafe, |
// Skip handler names from |
// http://www.w3.org/TR/html5/Overview.html#event-handlers-on-elements-document-objects-and-window-objects |
// since we have special handling in attrType. |
"open": contentTypePlain, |
"optimum": contentTypePlain, |
"pattern": contentTypeUnsafe, |
"placeholder": contentTypePlain, |
"poster": contentTypeURL, |
"profile": contentTypeURL, |
"preload": contentTypePlain, |
"pubdate": contentTypePlain, |
"radiogroup": contentTypePlain, |
"readonly": contentTypePlain, |
"rel": contentTypeUnsafe, |
"required": contentTypePlain, |
"reversed": contentTypePlain, |
"rows": contentTypePlain, |
"rowspan": contentTypePlain, |
"sandbox": contentTypeUnsafe, |
"spellcheck": contentTypePlain, |
"scope": contentTypePlain, |
"scoped": contentTypePlain, |
"seamless": contentTypePlain, |
"selected": contentTypePlain, |
"shape": contentTypePlain, |
"size": contentTypePlain, |
"sizes": contentTypePlain, |
"span": contentTypePlain, |
"src": contentTypeURL, |
"srcdoc": contentTypeHTML, |
"srclang": contentTypePlain, |
"start": contentTypePlain, |
"step": contentTypePlain, |
"style": contentTypeCSS, |
"tabindex": contentTypePlain, |
"target": contentTypePlain, |
"title": contentTypePlain, |
"type": contentTypeUnsafe, |
"usemap": contentTypeURL, |
"value": contentTypeUnsafe, |
"width": contentTypePlain, |
"wrap": contentTypePlain, |
"xmlns": contentTypeURL, |
} |
|
// attrType returns a conservative (upper-bound on authority) guess at the |
// type of the named attribute. |
func attrType(name string) contentType { |
name = strings.ToLower(name) |
if strings.HasPrefix(name, "data-") { |
// Strip data- so that custom attribute heuristics below are |
// widely applied. |
// Treat data-action as URL below. |
name = name[5:] |
} else if colon := strings.IndexRune(name, ':'); colon != -1 { |
if name[:colon] == "xmlns" { |
return contentTypeURL |
} |
// Treat svg:href and xlink:href as href below. |
name = name[colon+1:] |
} |
if t, ok := attrTypeMap[name]; ok { |
return t |
} |
// Treat partial event handler names as script. |
if strings.HasPrefix(name, "on") { |
return contentTypeJS |
} |
|
// Heuristics to prevent "javascript:..." injection in custom |
// data attributes and custom attributes like g:tweetUrl. |
// http://www.w3.org/TR/html5/elements.html#embedding-custom-non-visible-data-with-the-data-attributes: |
// "Custom data attributes are intended to store custom data |
// private to the page or application, for which there are no |
// more appropriate attributes or elements." |
// Developers seem to store URL content in data URLs that start |
// or end with "URI" or "URL". |
if strings.Contains(name, "src") || |
strings.Contains(name, "uri") || |
strings.Contains(name, "url") { |
return contentTypeURL |
} |
return contentTypePlain |
} |