Beispiel #1
0
////----------------------------
/// FFConfigParser::ParseDefault
//--------------------------------
//
//
//	Parameters:
//
//	Returns:
//
qboolean FFConfigParser::ParseDefaults( const char **pos )
{
	qboolean result = qboolean( pos != NULL );

	if ( pos )
	{
		char *token = COM_ParseExt( pos, qtrue );
		if ( token[ 0 ] == '{' )
		{
			for
			(	token = COM_ParseExt( pos, qtrue )
			;	token[ 0 ]
			&&	token[ 0 ] != '}'
			&&	result // fail if any problem
			;	token = COM_ParseExt( pos, qtrue )
			){
				int techType = 0;

				if ( sscanf( token, "%d", &techType ) )
				{
					TDeviceType &deviceType = mDefaultSet[ techType ];
					if ( !deviceType.size() )
					{
						result &= ParseDefault( pos, deviceType );
						mDefaultPriority.push_back( techType );
					}
					else
					{
						result = qfalse;
#ifdef FF_PRINT
						ConsoleParseError
						(	"Redefinition of TechType index"
						,	token
						);
#endif
					}
				}
				else
				{
					result = qfalse;
#ifdef FF_PRINT
					ConsoleParseError
					(	"TechType fields should begin with integers"
					,	token
					);
#endif
				}
			}
		}
		else
		{
			// expected '{'
			result = qfalse;
		}
	}

	return result;
}
Beispiel #2
0
sk_sp<sksg::Gradient> AttachGradient(const Json::Value& obj, AttachContext* ctx) {
    SkASSERT(obj.isObject());

    const auto& stops = obj["g"];
    if (!stops.isObject())
        return nullptr;

    const auto stopCount = ParseDefault(stops["p"], -1);
    if (stopCount < 0)
        return nullptr;

    sk_sp<sksg::Gradient> gradient_node;
    sk_sp<GradientAdapter> adapter;

    if (ParseDefault(obj["t"], 1) == 1) {
        auto linear_node = sksg::LinearGradient::Make();
        adapter = sk_make_sp<LinearGradientAdapter>(linear_node, stopCount);
        gradient_node = std::move(linear_node);
    } else {
        auto radial_node = sksg::RadialGradient::Make();
        adapter = sk_make_sp<RadialGradientAdapter>(radial_node, stopCount);

        // TODO: highlight, angle
        gradient_node = std::move(radial_node);
    }

    BindProperty<VectorValue>(stops["k"], &ctx->fAnimators,
        [adapter](const VectorValue& stops) {
            adapter->setColorStops(stops);
        });
    BindProperty<VectorValue>(obj["s"], &ctx->fAnimators,
        [adapter](const VectorValue& s) {
            adapter->setStartPoint(ValueTraits<VectorValue>::As<SkPoint>(s));
        });
    BindProperty<VectorValue>(obj["e"], &ctx->fAnimators,
        [adapter](const VectorValue& e) {
            adapter->setEndPoint(ValueTraits<VectorValue>::As<SkPoint>(e));
        });

    return gradient_node;
}
Beispiel #3
0
sk_sp<sksg::PaintNode> AttachStroke(const Json::Value& jstroke, AttachContext* ctx,
                                    sk_sp<sksg::PaintNode> stroke_node) {
    SkASSERT(jstroke.isObject());

    if (!stroke_node)
        return nullptr;

    stroke_node->setStyle(SkPaint::kStroke_Style);

    auto width_attached = BindProperty<ScalarValue>(jstroke["w"], &ctx->fAnimators,
        [stroke_node](const ScalarValue& w) {
            stroke_node->setStrokeWidth(w);
        });
    if (!width_attached)
        return nullptr;

    stroke_node->setStrokeMiter(ParseDefault(jstroke["ml"], 4.0f));

    static constexpr SkPaint::Join gJoins[] = {
        SkPaint::kMiter_Join,
        SkPaint::kRound_Join,
        SkPaint::kBevel_Join,
    };
    stroke_node->setStrokeJoin(gJoins[SkTPin<int>(ParseDefault(jstroke["lj"], 1) - 1,
                                                  0, SK_ARRAY_COUNT(gJoins) - 1)]);

    static constexpr SkPaint::Cap gCaps[] = {
        SkPaint::kButt_Cap,
        SkPaint::kRound_Cap,
        SkPaint::kSquare_Cap,
    };
    stroke_node->setStrokeCap(gCaps[SkTPin<int>(ParseDefault(jstroke["lc"], 1) - 1,
                                                0, SK_ARRAY_COUNT(gCaps) - 1)]);

    return stroke_node;
}
Beispiel #4
0
sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const Json::Value& jstar, AttachContext* ctx) {
    SkASSERT(jstar.isObject());

    static constexpr PolyStarAdapter::Type gTypes[] = {
        PolyStarAdapter::Type::kStar, // "sy": 1
        PolyStarAdapter::Type::kPoly, // "sy": 2
    };

    const auto type = ParseDefault(jstar["sy"], 0) - 1;
    if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gTypes))) {
        LogFail(jstar, "Unknown polystar type");
        return nullptr;
    }

    auto path_node = sksg::Path::Make();
    auto adapter = sk_make_sp<PolyStarAdapter>(path_node, gTypes[type]);

    BindProperty<VectorValue>(jstar["p"], &ctx->fAnimators,
        [adapter](const VectorValue& p) {
            adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
        });
    BindProperty<ScalarValue>(jstar["pt"], &ctx->fAnimators,
        [adapter](const ScalarValue& pt) {
            adapter->setPointCount(pt);
        });
    BindProperty<ScalarValue>(jstar["ir"], &ctx->fAnimators,
        [adapter](const ScalarValue& ir) {
            adapter->setInnerRadius(ir);
        });
    BindProperty<ScalarValue>(jstar["or"], &ctx->fAnimators,
        [adapter](const ScalarValue& otr) {
            adapter->setOuterRadius(otr);
        });
    BindProperty<ScalarValue>(jstar["is"], &ctx->fAnimators,
        [adapter](const ScalarValue& is) {
            adapter->setInnerRoundness(is);
        });
    BindProperty<ScalarValue>(jstar["os"], &ctx->fAnimators,
        [adapter](const ScalarValue& os) {
            adapter->setOuterRoundness(os);
        });
    BindProperty<ScalarValue>(jstar["r"], &ctx->fAnimators,
        [adapter](const ScalarValue& r) {
            adapter->setRotation(r);
        });

    return path_node;
}
Beispiel #5
0
std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
    const Json::Value& jmerge, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
    std::vector<sk_sp<sksg::GeometryNode>> merged;

    static constexpr sksg::Merge::Mode gModes[] = {
        sksg::Merge::Mode::kMerge,      // "mm": 1
        sksg::Merge::Mode::kUnion,      // "mm": 2
        sksg::Merge::Mode::kDifference, // "mm": 3
        sksg::Merge::Mode::kIntersect,  // "mm": 4
        sksg::Merge::Mode::kXOR      ,  // "mm": 5
    };

    const auto mode = gModes[SkTPin<int>(ParseDefault(jmerge["mm"], 1) - 1,
                                         0, SK_ARRAY_COUNT(gModes) - 1)];
    merged.push_back(sksg::Merge::Make(std::move(geos), mode));

    return merged;
}
Beispiel #6
0
/*************************************************************
  NAME         : ParseDefaultFacet
  DESCRIPTION  : Parses the facet for a slot
  INPUTS       : 1) The input logical name
                 2) The bitmap indicating which facets have
                    already been parsed
                 3) The slot descriptor to set
  RETURNS      : True if all OK, false otherwise
  SIDE EFFECTS : Slot  set and parsed facet bitmap set
  NOTES        : Syntax: (default ?NONE|<expression>*)
                         (default-dynamic <expression>*)
 *************************************************************/
static bool ParseDefaultFacet(
  Environment *theEnv,
  const char *readSource,
  char *specbits,
  SlotDescriptor *slot)
  {
   Expression *tmp;
   bool error, noneSpecified, deriveSpecified;

   if (TestBitMap(specbits,DEFAULT_BIT))
     {
      PrintErrorID(theEnv,"CLSLTPSR",2,false);
      WriteString(theEnv,STDERR,"The 'default' facet for slot '");
      WriteString(theEnv,STDERR,slot->slotName->name->contents);
      WriteString(theEnv,STDERR,"' is already specified.\n");
      return false;
     }
   SetBitMap(specbits,DEFAULT_BIT);
   error = false;
   tmp = ParseDefault(theEnv,readSource,true,TestBitMap(specbits,DEFAULT_DYNAMIC_BIT),
                      false,&noneSpecified,&deriveSpecified,&error);
   if (error == true)
     return false;
   if (noneSpecified || deriveSpecified)
     {
     if (noneSpecified)
       {
        slot->noDefault = 1;
        slot->defaultSpecified = 1;
       }
      else
        ClearBitMap(specbits,DEFAULT_BIT);
     }
   else
     {
      slot->defaultValue = PackExpression(theEnv,tmp);
      ReturnExpression(theEnv,tmp);
      ExpressionInstall(theEnv,(Expression *) slot->defaultValue);
      slot->defaultSpecified = 1;
     }
   return true;
  }
Beispiel #7
0
std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
    const Json::Value& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {

    enum class Mode {
        kMerged,   // "m": 1
        kSeparate, // "m": 2
    } gModes[] = { Mode::kMerged, Mode::kSeparate };

    const auto mode = gModes[SkTPin<int>(ParseDefault(jtrim["m"], 1) - 1,
                                         0, SK_ARRAY_COUNT(gModes) - 1)];

    std::vector<sk_sp<sksg::GeometryNode>> inputs;
    if (mode == Mode::kMerged) {
        inputs.push_back(sksg::Merge::Make(std::move(geos), sksg::Merge::Mode::kMerge));
    } else {
        inputs = std::move(geos);
    }

    std::vector<sk_sp<sksg::GeometryNode>> trimmed;
    trimmed.reserve(inputs.size());
    for (const auto& i : inputs) {
        const auto trimEffect = sksg::TrimEffect::Make(i);
        trimmed.push_back(trimEffect);

        const auto adapter = sk_make_sp<TrimEffectAdapter>(std::move(trimEffect));
        BindProperty<ScalarValue>(jtrim["s"], &ctx->fAnimators,
            [adapter](const ScalarValue& s) {
                adapter->setStart(s);
            });
        BindProperty<ScalarValue>(jtrim["e"], &ctx->fAnimators,
            [adapter](const ScalarValue& e) {
                adapter->setEnd(e);
            });
        BindProperty<ScalarValue>(jtrim["o"], &ctx->fAnimators,
            [adapter](const ScalarValue& o) {
                adapter->setOffset(o);
            });
    }

    return trimmed;
}
Beispiel #8
0
/*************************************************************
  NAME         : ParseDefaultFacet
  DESCRIPTION  : Parses the facet for a slot
  INPUTS       : 1) The input logical name
                 2) The bitmap indicating which facets have
                    already been parsed
                 3) The slot descriptor to set
  RETURNS      : TRUE if all OK, FALSE otherwise
  SIDE EFFECTS : Slot  set and parsed facet bitmap set
  NOTES        : Syntax: (default ?NONE|<expression>*)
                         (default-dynamic <expression>*)
 *************************************************************/
static intBool ParseDefaultFacet(
  void *theEnv,
  EXEC_STATUS,
  char *readSource,
  char *specbits,
  SLOT_DESC *slot)
  {
   EXPRESSION *tmp;
   int error,noneSpecified,deriveSpecified;

   if (TestBitMap(specbits,DEFAULT_BIT))
     {
      PrintErrorID(theEnv,execStatus,"CLSLTPSR",2,FALSE);
      EnvPrintRouter(theEnv,execStatus,WERROR,"default facet already specified.\n");
      return(FALSE);
     }
   SetBitMap(specbits,DEFAULT_BIT);
   error = FALSE;
   tmp = ParseDefault(theEnv,execStatus,readSource,1,(int) TestBitMap(specbits,DEFAULT_DYNAMIC_BIT),
                      0,&noneSpecified,&deriveSpecified,&error);
   if (error == TRUE)
     return(FALSE);
   if (noneSpecified || deriveSpecified)
     {
     if (noneSpecified)
       {
        slot->noDefault = 1;
        slot->defaultSpecified = 1;
       }
      else
        ClearBitMap(specbits,DEFAULT_BIT);
     }
   else
     {
      slot->defaultValue = (void *) PackExpression(theEnv,execStatus,tmp);
      ReturnExpression(theEnv,execStatus,tmp);
      ExpressionInstall(theEnv,execStatus,(EXPRESSION *) slot->defaultValue);
      slot->defaultSpecified = 1;
     }
   return(TRUE);
  }
Beispiel #9
0
static void ParseStatement (scontext_t owner)
{
	if (StatementIndex == MAX_STATEMENT_DEPTH)
	{
		PR_ParseError("statement overflow");
	}
	ContextHistory[StatementIndex++] = owner;

	if (TK_CHECK(TK_LBRACE))
	{
		ContextLevel += EnterContext[owner];
		do
		{
			ParseStatement(owner);
		} while (!TK_CHECK(TK_RBRACE));

		ContextLevel -= EnterContext[owner];
		StatementIndex--;
		return;
	}

	if (TK_CHECK(TK_SEMICOLON))
	{
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("return"))
	{
		ParseReturn();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("loop"))
	{
		ParseLoop();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("while"))
	{
		ParseWhile();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("until"))
	{
		ParseUntil();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("do"))
	{
		ParseDo();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("switch"))
	{
		ParseSwitch();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("case"))
	{
		if (owner != SCONTEXT_SWITCH)
		{
			PR_ParseError("misplaced case");
		}
		ParseCase();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("break"))
	{
		if (BreakAncestor() == false)
		{
			PR_ParseError("misplaced break");
		}
		ParseBreak();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("continue"))
	{
		if (ContinueAncestor() == false)
		{
			PR_ParseError("misplaced continue");
		}
		ParseContinue();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("default"))
	{
		ParseDefault();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("thinktime"))
	{
		ParseThinktime();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("local"))
	{
		ParseLocalDefs();
		StatementIndex--;
		return;
	}
	if (LX_Check("float") || LX_Check("vector")
		|| LX_Check("entity") || LX_Check("string")
		|| LX_Check("void"))
	{
		ParseLocalDefs();
		StatementIndex--;
		return;
	}
	if (LX_CheckFetch("if"))
	{
		ParseIf();
		StatementIndex--;
		return;
	}

	EX_Expression(TOP_PRIORITY);
	LX_Require(";");
	StatementIndex--;
}
Beispiel #10
0
static struct templateSlot *DefinedSlots(
  void *theEnv,
  char *readSource,
  SYMBOL_HN *slotName,
  int multifieldSlot,
  struct token *inputToken)
  {
   struct templateSlot *newSlot;
   struct expr *defaultList;
   int defaultFound = FALSE;
   int noneSpecified, deriveSpecified;
   CONSTRAINT_PARSE_RECORD parsedConstraints;

   /*===========================*/
   /* Build the slot container. */
   /*===========================*/

   newSlot = get_struct(theEnv,templateSlot);
   newSlot->slotName = slotName;
   newSlot->defaultList = NULL;
   newSlot->constraints = GetConstraintRecord(theEnv);
   if (multifieldSlot)
     { newSlot->constraints->multifieldsAllowed = TRUE; }
   newSlot->multislot = multifieldSlot;
   newSlot->noDefault = FALSE;
   newSlot->defaultPresent = FALSE;
   newSlot->defaultDynamic = FALSE;
   newSlot->next = NULL;

   /*========================================*/
   /* Parse the primitive slot if it exists. */
   /*========================================*/

   InitializeConstraintParseRecord(&parsedConstraints);
   GetToken(theEnv,readSource,inputToken);

   while (inputToken->type != RPAREN)
     {
      PPBackup(theEnv);
      SavePPBuffer(theEnv," ");
      SavePPBuffer(theEnv,inputToken->printForm);

      /*================================================*/
      /* Slot attributes begin with a left parenthesis. */
      /*================================================*/

      if (inputToken->type != LPAREN)
        {
         SyntaxErrorMessage(theEnv,"deftemplate");
         ReturnSlots(theEnv,newSlot);
         DeftemplateData(theEnv)->DeftemplateError = TRUE;
         return(NULL);
        }

      /*=============================================*/
      /* The name of the attribute must be a symbol. */
      /*=============================================*/

      GetToken(theEnv,readSource,inputToken);
      if (inputToken->type != SYMBOL)
        {
         SyntaxErrorMessage(theEnv,"deftemplate");
         ReturnSlots(theEnv,newSlot);
         DeftemplateData(theEnv)->DeftemplateError = TRUE;
         return(NULL);
        }

      /*================================================================*/
      /* Determine if the attribute is one of the standard constraints. */
      /*================================================================*/

      if (StandardConstraint(ValueToString(inputToken->value)))
        {
         if (ParseStandardConstraint(theEnv,readSource,(ValueToString(inputToken->value)),
                                     newSlot->constraints,&parsedConstraints,
                                     multifieldSlot) == FALSE)
           {
            DeftemplateData(theEnv)->DeftemplateError = TRUE;
            ReturnSlots(theEnv,newSlot);
            return(NULL);
           }
        }

      /*=================================================*/
      /* else if the attribute is the default attribute, */
      /* then get the default list for this slot.        */
      /*=================================================*/

      else if ((strcmp(ValueToString(inputToken->value),"default") == 0) ||
               (strcmp(ValueToString(inputToken->value),"default-dynamic") == 0))
        {
         /*======================================================*/
         /* Check to see if the default has already been parsed. */
         /*======================================================*/

         if (defaultFound)
           {
            AlreadyParsedErrorMessage(theEnv,"default attribute",NULL);
            DeftemplateData(theEnv)->DeftemplateError = TRUE;
            ReturnSlots(theEnv,newSlot);
            return(NULL);
           }

         newSlot->noDefault = FALSE;

         /*=====================================================*/
         /* Determine whether the default is dynamic or static. */
         /*=====================================================*/

         if (strcmp(ValueToString(inputToken->value),"default") == 0)
           {
            newSlot->defaultPresent = TRUE;
            newSlot->defaultDynamic = FALSE;
           }
         else
           {
            newSlot->defaultPresent = FALSE;
            newSlot->defaultDynamic = TRUE;
           }

         /*===================================*/
         /* Parse the list of default values. */
         /*===================================*/

         defaultList = ParseDefault(theEnv,readSource,multifieldSlot,(int) newSlot->defaultDynamic,
                                  TRUE,&noneSpecified,&deriveSpecified,&DeftemplateData(theEnv)->DeftemplateError);
         if (DeftemplateData(theEnv)->DeftemplateError == TRUE)
           {
            ReturnSlots(theEnv,newSlot);
            return(NULL);
           }

         /*==================================*/
         /* Store the default with the slot. */
         /*==================================*/

         defaultFound = TRUE;
         if (deriveSpecified) newSlot->defaultPresent = FALSE;
         else if (noneSpecified)
           {
            newSlot->noDefault = TRUE;
            newSlot->defaultPresent = FALSE;
           }
         newSlot->defaultList = defaultList;
        }

      /*============================================*/
      /* Otherwise the attribute is an invalid one. */
      /*============================================*/

      else
        {
         SyntaxErrorMessage(theEnv,"slot attributes");
         ReturnSlots(theEnv,newSlot);
         DeftemplateData(theEnv)->DeftemplateError = TRUE;
         return(NULL);
        }

      /*===================================*/
      /* Begin parsing the next attribute. */
      /*===================================*/

      GetToken(theEnv,readSource,inputToken);
     }

   /*============================*/
   /* Return the attribute list. */
   /*============================*/

   return(newSlot);
  }
Beispiel #11
0
static void ParseOuter (FScanner &sc)
{
	int bracedepth = 0;
	bool ifskip = false;

	while (sc.GetString ())
	{
		if (ifskip)
		{
			if (bracedepth > 0)
			{
				if (sc.Compare ("}"))
				{
					bracedepth--;
					continue;
				}
			}
			else if (sc.Compare ("endif"))
			{
				ifskip = false;
				continue;
			}
			if (sc.Compare ("{"))
			{
				bracedepth++;
			}
			else if (sc.Compare ("}"))
			{
				sc.ScriptError ("Too many left braces ('}')");
			}
		}
		else
		{
			switch (sc.MustMatchString (OuterKeywords))
			{
			case OUT_SPLASH:
				ParseSplash (sc);
				break;

			case OUT_TERRAIN:
				ParseTerrain (sc);
				break;

			case OUT_FLOOR:
				ParseFloor (sc);
				break;

			case OUT_DEFAULTTERRAIN:
				ParseDefault (sc);
				break;

			case OUT_IFDOOM:
			case OUT_IFHERETIC:
			case OUT_IFHEXEN:
			case OUT_IFSTRIFE:
				ifskip = !CheckGame(sc.String+2, true);
				break;

			case OUT_ENDIF:
				break;
			}
		}
	}
}
Beispiel #12
0
void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts,
                                  const skjson::ArrayValue* jchars) {
    // Optional array of font entries, referenced (by name) from text layer document nodes. E.g.
    // "fonts": {
    //        "list": [
    //            {
    //                "ascent": 75,
    //                "fClass": "",
    //                "fFamily": "Roboto",
    //                "fName": "Roboto-Regular",
    //                "fPath": "https://fonts.googleapis.com/css?family=Roboto",
    //                "fPath": "",
    //                "fStyle": "Regular",
    //                "fWeight": "",
    //                "origin": 1
    //            }
    //        ]
    //    },
    if (jfonts) {
        if (const skjson::ArrayValue* jlist = (*jfonts)["list"]) {
            for (const skjson::ObjectValue* jfont : *jlist) {
                if (!jfont) {
                    continue;
                }

                const skjson::StringValue* jname   = (*jfont)["fName"];
                const skjson::StringValue* jfamily = (*jfont)["fFamily"];
                const skjson::StringValue* jstyle  = (*jfont)["fStyle"];
                const skjson::StringValue* jpath   = (*jfont)["fPath"];

                if (!jname   || !jname->size() ||
                    !jfamily || !jfamily->size() ||
                    !jstyle  || !jstyle->size()) {
                    this->log(Logger::Level::kError, jfont, "Invalid font.");
                    continue;
                }

                const auto& fmgr = fLazyFontMgr.get();

                // Typeface fallback order:
                //   1) externally-loaded font (provided by the embedder)
                //   2) system font (family/style)
                //   3) system default

                sk_sp<SkTypeface> tf =
                    fmgr->makeFromData(fResourceProvider->loadFont(jname->begin(),
                                                                   jpath ? jpath->begin()
                                                                         : nullptr));

                if (!tf) {
                    tf.reset(fmgr->matchFamilyStyle(jfamily->begin(),
                                                    FontStyle(this, jstyle->begin())));
                }

                if (!tf) {
                    this->log(Logger::Level::kError, nullptr,
                              "Could not create typeface for %s|%s.",
                              jfamily->begin(), jstyle->begin());
                    // Last resort.
                    tf = fmgr->legacyMakeTypeface(nullptr, FontStyle(this, jstyle->begin()));
                    if (!tf) {
                        continue;
                    }
                }

                fFonts.set(SkString(jname->begin(), jname->size()),
                          {
                              SkString(jfamily->begin(), jfamily->size()),
                              SkString(jstyle->begin(), jstyle->size()),
                              ParseDefault((*jfont)["ascent"] , 0.0f),
                              std::move(tf)
                          });
            }
        }
    }

    // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
    // "chars": [
    //     {
    //         "ch": "t",
    //         "data": {
    //             "shapes": [...]
    //         },
    //         "fFamily": "Roboto",
    //         "size": 50,
    //         "style": "Regular",
    //         "w": 32.67
    //    }
    // ]
    if (jchars) {
        FontInfo* current_font = nullptr;

        for (const skjson::ObjectValue* jchar : *jchars) {
            if (!jchar) {
                continue;
            }

            const skjson::StringValue* jch = (*jchar)["ch"];
            if (!jch) {
                continue;
            }

            const skjson::StringValue* jfamily = (*jchar)["fFamily"];
            const skjson::StringValue* jstyle  = (*jchar)["style"]; // "style", not "fStyle"...

            const auto* ch_ptr = jch->begin();
            const auto  ch_len = jch->size();

            if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
                this->log(Logger::Level::kError, jchar, "Invalid glyph.");
                continue;
            }

            const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
            SkASSERT(uni != -1);

            const auto* family = jfamily->begin();
            const auto* style  = jstyle->begin();

            // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
            // (family, style) -- not by name :(  For now this performs a linear search over *all*
            // fonts: generally there are few of them, and glyph definitions are font-clustered.
            // If problematic, we can refactor as a two-level hashmap.
            if (!current_font || !current_font->matches(family, style)) {
                current_font = nullptr;
                fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
                    if (finfo->matches(family, style)) {
                        current_font = finfo;
                        // TODO: would be nice to break early here...
                    }
                });
                if (!current_font) {
                    this->log(Logger::Level::kError, nullptr,
                              "Font not found for codepoint (%d, %s, %s).", uni, family, style);
                    continue;
                }
            }

            // TODO: parse glyphs
        }
    }
}