From 2f9dcb2d21d1989756770b69552b27018c6a7322 Mon Sep 17 00:00:00 2001 From: sh0rez Date: Mon, 27 Jul 2020 16:59:04 +0200 Subject: [PATCH] feat: document plain values (#12) * feat: document plain values Adds `d.val` to attach type and help information to plain Jsonnet values, apart from specially treated `fn` and `obj`. * feat: defaults --- doc-util/README.md | 25 ++++++++++++++++++++++++- doc-util/main.libsonnet | 12 ++++++++++++ main.go | 3 ++- pkg/docsonnet/fast.go | 27 ++++++++++++++++++++++++++- pkg/docsonnet/load.go | 16 ++++++++++------ pkg/docsonnet/model.go | 3 ++- pkg/render/render.go | 22 ++++++++++++++++++++++ pkged.go | 2 +- 8 files changed, 99 insertions(+), 11 deletions(-) diff --git a/doc-util/README.md b/doc-util/README.md index 162415b..2621930 100644 --- a/doc-util/README.md +++ b/doc-util/README.md @@ -18,6 +18,7 @@ local d = import "github.com/sh0rez/docsonnet/doc-util" * [`fn fn(help, args)`](#fn-fn) * [`fn obj(help, fields)`](#fn-obj) * [`fn pkg(name, url, help)`](#fn-pkg) +* [`fn val(type, help, default)`](#fn-val) * [`obj argument`](#obj-argument) * [`fn new(name, type, default)`](#fn-argumentnew) * [`obj func`](#obj-func) @@ -29,6 +30,8 @@ local d = import "github.com/sh0rez/docsonnet/doc-util" * [`fn withFields(fields)`](#fn-objectwithfields) * [`obj package`](#obj-package) * [`fn new(name, url, help)`](#fn-packagenew) +* [`obj value`](#obj-value) + * [`fn new(type, help, default)`](#fn-valuenew) ## Fields @@ -64,6 +67,14 @@ pkg(name, url, help) `new` is a shorthand for `package.new` +### fn val + +```ts +val(type, help, default) +``` + +`val` is a shorthand for `value.new` + ## obj argument Utilities for creating function arguments @@ -134,4 +145,16 @@ The `withFields` modifier overrides the fields property of an already created ob new(name, url, help) ``` -new creates a new package with given `name`, `import` URL and `help` text \ No newline at end of file +new creates a new package with given `name`, `import` URL and `help` text + +## obj value + +Utilities for documenting plain Jsonnet values (primitives) + +### fn value.new + +```ts +new(type, help, default) +``` + +new creates a new object of given type, optionally with description and default value \ No newline at end of file diff --git a/doc-util/main.libsonnet b/doc-util/main.libsonnet index 60cc669..596289d 100644 --- a/doc-util/main.libsonnet +++ b/doc-util/main.libsonnet @@ -73,6 +73,18 @@ '#arg': self.argument['#new'] + self.func.withHelp('`arg` is a shorthand for `argument.new`'), arg:: self.argument.new, + "#value": d.obj("Utilities for documenting plain Jsonnet values (primitives)"), + value:: { + "#new": d.fn("new creates a new object of given type, optionally with description and default value", [d.arg("type", d.T.string), d.arg("help", d.T.string), d.arg("default", d.T.any)]), + new(type, help='', default=null): { 'value': { + help: help, + type: type, + default: default, + } } + }, + '#val': self.value['#new'] + self.func.withHelp('`val` is a shorthand for `value.new`'), + val: self.value.new, + // T contains constants for the Jsonnet types T:: { string: 'string', diff --git a/main.go b/main.go index ea26f69..ac3c308 100644 --- a/main.go +++ b/main.go @@ -24,12 +24,13 @@ func main() { outputJSON := root.Flags().Bool("json", false, "print loaded docsonnet as JSON") outputRaw := root.Flags().Bool("raw", false, "don't transform, dump raw eval result") urlPrefix := root.Flags().String("urlPrefix", "/", "url-prefix for frontmatter") + jpath := root.Flags().StringSliceP("jpath", "J", []string{"vendor"}, "Specify an additional library search dir (right-most wins)") root.Run = func(cmd *cli.Command, args []string) error { file := args[0] log.Println("Extracting from Jsonnet") - data, err := docsonnet.Extract(file) + data, err := docsonnet.Extract(file, docsonnet.Opts{JPath: *jpath}) if err != nil { log.Fatalln("Extracting:", err) } diff --git a/pkg/docsonnet/fast.go b/pkg/docsonnet/fast.go index ada9f44..7210277 100644 --- a/pkg/docsonnet/fast.go +++ b/pkg/docsonnet/fast.go @@ -94,7 +94,32 @@ func loadField(name string, field map[string]interface{}, parent map[string]inte return loadObj(name, iobj.(map[string]interface{}), parent) } - panic(fmt.Sprintf("field %s lacking {function | object}", name)) + if vobj, ok := field["value"]; ok { + return loadValue(name, vobj.(map[string]interface{})) + } + + panic(fmt.Sprintf("field %s lacking {function | object | value}", name)) +} + +func loadValue(name string, msi map[string]interface{}) Field { + h, ok := msi["help"].(string) + if !ok { + h = "" + } + + t, ok := msi["type"].(string) + if !ok { + panic(fmt.Sprintf("value %s lacking type information", name)) + } + + v := Value{ + Name: name, + Help: h, + Type: Type(t), + Default: msi["default"], + } + + return Field{Value: &v} } func loadFn(name string, msi map[string]interface{}) Field { diff --git a/pkg/docsonnet/load.go b/pkg/docsonnet/load.go index f2a9666..1bf643d 100644 --- a/pkg/docsonnet/load.go +++ b/pkg/docsonnet/load.go @@ -10,10 +10,14 @@ import ( "github.com/markbates/pkger" ) +type Opts struct { + JPath []string +} + // Load extracts and transforms the docsonnet data in `filename`, returning the // top level docsonnet package. -func Load(filename string) (*Package, error) { - data, err := Extract(filename) +func Load(filename string, opts Opts) (*Package, error) { + data, err := Extract(filename, opts) if err != nil { return nil, err } @@ -25,7 +29,7 @@ func Load(filename string) (*Package, error) { // information, exactly as they appear in Jsonnet. Keep in mind this // representation is usually not suitable for any use, use `Transform` to // convert it to the familiar docsonnet data model. -func Extract(filename string) ([]byte, error) { +func Extract(filename string, opts Opts) ([]byte, error) { // get load.libsonnet from embedded data file, err := pkger.Open("/load.libsonnet") if err != nil { @@ -38,7 +42,7 @@ func Extract(filename string) ([]byte, error) { // setup Jsonnet vm vm := jsonnet.MakeVM() - importer, err := newImporter() + importer, err := newImporter(opts.JPath) if err != nil { return nil, err } @@ -74,7 +78,7 @@ type importer struct { util jsonnet.Contents } -func newImporter() (*importer, error) { +func newImporter(paths []string) (*importer, error) { file, err := pkger.Open("/doc-util/main.libsonnet") if err != nil { return nil, err @@ -85,7 +89,7 @@ func newImporter() (*importer, error) { } return &importer{ - fi: jsonnet.FileImporter{}, + fi: jsonnet.FileImporter{JPaths: paths}, util: jsonnet.MakeContents(string(load)), }, nil } diff --git a/pkg/docsonnet/model.go b/pkg/docsonnet/model.go index d4f3db3..8e4f3f6 100644 --- a/pkg/docsonnet/model.go +++ b/pkg/docsonnet/model.go @@ -41,7 +41,8 @@ type Value struct { Name string `json:"-"` Help string `json:"help"` - Type Type `json:"type"` + Type Type `json:"type"` + Default interface{} `json:"default"` } // Type is a Jsonnet type diff --git a/pkg/render/render.go b/pkg/render/render.go index c33db0b..d0bb2af 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -119,6 +119,11 @@ func renderIndex(api docsonnet.Fields, path string, s *slug.Slugger) []md.Elem { link := "#" + s.Slug("obj "+path+obj.Name) elems = append(elems, md.Link(md.Code(name), link)) elems = append(elems, md.List(renderIndex(obj.Fields, path+obj.Name+".", s)...)) + case v.Value != nil: + val := v.Value + name := md.Text(fmt.Sprintf("%s %s%s", val.Type, path, val.Name)) + link := "#" + s.Slug(name.String()) + elems = append(elems, md.Link(md.Code(name), link)) } } return elems @@ -144,6 +149,23 @@ func renderApi(api docsonnet.Fields, path string) []md.Elem { md.Text(obj.Help), ) elems = append(elems, renderApi(obj.Fields, path+obj.Name+".")...) + + case v.Value != nil: + val := v.Value + elems = append(elems, + md.Headline(3, fmt.Sprintf("%s %s%s", val.Type, path, val.Name)), + ) + + if val.Default != nil { + elems = append(elems, md.Paragraph( + md.Italic(md.Text("Default value: ")), + md.Code(md.Text(fmt.Sprint(val.Default))), + )) + } + + elems = append(elems, + md.Text(val.Help), + ) } } diff --git a/pkged.go b/pkged.go index afbfd77..29957fb 100644 --- a/pkged.go +++ b/pkged.go @@ -9,4 +9,4 @@ import ( "github.com/markbates/pkger/pkging/mem" ) -var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec7b5b93aab8f6f85739c5ebdfd9025ebab5eaffd0b0b78876dbbbb5b708a7a67641c040131286808a53fbbbff2ae1aa7d73ce999733e5434bb2b248d67d258bf49f4280b7840ae33f0518a47ee67c0124ea525f4cbc63d72580128cbd940d7f0d12612c747d1279ddd4496c1b84dd3d49c2165647d0a39824e9773bf585f187137684851d79c25888ec000b1de12b01c258103ac2b39d40b6e0d94a90749d009f4cb024e435de2b8a1eec14f8c2f8dfc217e1f78eb04a6de409e3ad8da857f6969e4d092e16d7c824401e65e88cae2f90081d210ea1e7b2e6ef157f1cc1c380b80186dd17f67a47d8466cb916cf90fc065070e802149c0e4476123a76ead12e9b3af97090fdb245222f3ac53b9728433c61fc33dcc4c32e5f1c11c858fbeac59c2f27db068c6d274f3d2a740440a238f128ed6e919d7a6d003c0631efe3d40eb097745140d312e01d782bc9e394d48dae5dcc58744010fb9c80b2efb6075d6a371d0f9c765d79309046af00dd00a75e826dd4f5dcbd9db8f41c0da1204e03d040fcc86ef5ead7131bbb591aa0378668e6a4c86b062277d074d87bad1ee8b73a6d06a86f4b273d79303ce90f24b9d53f5b32452d391d06e2e8b4d78dc3e020746afb6c35bb36c552bbefd8d41bf64f2001b693bc0d0174d7eefa5e7bf2caf8eb7eccedd44b1292302ab7c8867fc9372071b2edd646a4eb7b89773e4620f2d8bb2f6f59f9abe1ae4d2f41811efe0c6b4b92c84ed3735f7d8dd858602581cbd0fffa02b19dd0bf844e2fa7264e084cecb380e3dbd40f004962c6db3eb1e3f78621f92dca501a7011fc1791af2627b2637a216a6a871ec1dd37517d2fb6ffa669badb00a5deb97edf0edd1785772f723cf73f4b04efe0d1d42567f4c5847a2c4cb3008ebcd4fb78b40b22f7738c6e80696a23f43727a7f3a53fce609f2152949da981c65ba9d78dcbf004491cc22f01eee67684beec58ec65e65c3eba20013d06abcd83a5461bc13608c459bbbb8d524a92b40dc25e9a2636f0da30422b27697c8f7061b672d1e92b89b7451e4851909e80698021f2b62880fec9aa34a7c046a8eb1d3ce0e1dd5b4319e6f9a2317e8fa66c4fd011f83e2020dd8094d9b000476c7b573cba4e009b664aab769909a320f2ca4797c784d8e642e1803f32927a6e9c0438b51d9ee00adb605af3d3346e35f94f25bd1a58515cc2d89e234e08df6fb07e96b011ee0484720114adac889b71c1057b3077f6ca7e295fde82de21ae1b5d9ae3d466924a329c168c95ad2ee01bc5aae77a0eb7b7aa5f4bd64e49c4b71faf464a91be82d39c915f9a124d1340b80e699a0418f2a11c83f2d14c5f6a56e8084c26ddd48be272ef76d22f72088316dc643800c46db5ba59ba9586a7fd5bdea5f696e1ed3cec92a40b09b231fc4212d83d74abdd9d6f03df96c5cbb0628272a9270e3ec1e60fe67d97e2553b9e8f90b364e7553bc50ff0fcd0dd7e8cf17a93f801f2271c33037631657f9147a90ddf9beec44560c65df053bc382187fc1344b9ebc736083fc00a5c6cbf334c735a86c4b746b915520f6489d775023748b277a555186c6263cab6481f215536ca26bc040fb3f97eef08cf1e4deb731fce102a40f549af003d10971139fe53b8e870fbc04eb5e334c9bcce6507678d3c10f713b42e245f22e272ecb597d0801f5ba52f525ff8f5eb5747d8163c7c78921fb3e66f2c9c77f90917054e73c40ff096b0a7eba57680f854b839a5b7703b020d8e9e30ee8972af23442c688cfbb2c89b3f7934190bb2288bbf8983dfa4deb3248de5feb8277e19de8a37b7c31bf9f6ff89f25864b121a03f5d269ff244cee2ddf84fe1abb713c6c3614f1c76041d1361dcbbbd19ddcae26d4758a00087c258e22a6124f4a4dbdb8ef0237085b1248a6247d09ae6e6e7cfd87645612c7684a5cb26153bc2aa45ba82c28293be381ab22e012115c6b71de12e0d2246caca03c2581adc8e7a43e9f646ec080bca2037625f92462351fad5111e3e46ad99fed511d4cb51373f7f6638a39e2b8cff2d76c48ef83b57323b325f548da9157d5e96696a2f2d8cd7f597a6c2d254510a5fb8b48852609f3952516668fcec1fe07ae55e821118c24beb5eeffae1af8ee0daa92d8c056f4fa0ae1d76a63ca1baf604bfaf94a3651ca2fb3b32d7d53bace760a86b4f19d0523cbf235057efa0134d52eb59c4960aa81adc415d552410ed4733d98d5dcd97cc60f0e2c8e20ec8700fa2f5f05e5eeeccde62e74403e4aaa3d0911f5277b38c9de67d621987fdf7cde1765baea1ab0ab4b4d1cbfd662ddac601ea5325777a566c69eba3ae7d832b5ccda7c40e5e221059be29ff805634ca754d091d797174e441666d96f0be9ef30e9a2b650ee451e6446b51ff36795aad18ee03b4e475666d66beab8d727dba24e6e609ba9b0502b97274a733c9ec2d2510ad435d5bfaaef60ddac6e0e86a6bdf52959d15282f8e2ca596311041de9ff3b5a6152f21552161f4fba69cfa96fc63f818283705df77589707c8cd018385f79195cd5537b336009abd1932374bb6beaf6b0c47d99bc622310d17e9532667082d19c996d187a636f01d630def5505da86b4777a33d15495f58f700f4d6310ea5a215f5355446be38bb37c0f0df929333733ccd6330d093198a50e7edce3850822945979c8eca1c261fac5f7aa12dea3a70cf496b96d0cf07cc5dff14104c84cf69133bd7b8db316e3fbda6ed6bd39a7f507d5a7ebdc51f7d0d6d614a8615b26502f7086e593d670d58d9d8db203f8093f06dce69a318daf3f2c9f955d8df8da53b1d0810a5e981dcf8e64a84f17c8d1cc0c6893175b9e606b4d4b5db8aeae52c657e41a83977bcc6d5a67f29bab2ee7c1549598eb65ba204cd6b636c92c55899c9e0ecd46cf59810b0afea74a62bdb5ee8ad928a8e973e4d91f96b1106747c268d89911213389fb40ec6a88d9035f87f9866b48c8c1cbd88940db1f76661423b3b73ceaaa0fbd5c19996a98cdf290cba49afff18540eefbea1d9ce50b4603bec006cbb99fa8ae8df6ae86764e34a1ce3484ae8c445b55426bb37801112ac602c577a22768450839daf2786677dc263fb1bbc836d6d49a3ed47655d1ffdaaeb8de47b31c505db362cb3884e028dd6c57856de9da28b0a3f58bfbb5e1bb8a3b8f41f16ce28452ad3bace7caf7a55f8b70bbdacf6bffed71fbd02a3a5b32fc616b3fa0396d8f2fa0ae493b4b436cfe5cd746b2b599e5b6c1652c32fc4a5645bc5310c0cb81ae8d225d9b64ba36a1205afbd634ac7563d5fa7e6232721a7face8065c9e6b6e4b1c2fdeacc2ca3f7af6664996e59af39ad73399a974a84fe9bc8aa1158d8f412da7dadfb8af0577b58c66f982d930934b9157a272cec985fe26cffef8d0df6a1a991fb9b06de7ccc681bca65650f3ce7db2ce6db2253951a5b351600710af595ed290681b2ccf583b10f05c2239c63a7335669fadfca149c8d5fc9d357d80738dcdb5106d6394819ce784da0fe7ab3206709c8737e33fd39115f5c98cf79517a667b7cc758c66bd35ffbdaaecc074193bf2c077b4c3a0ce07dafa68f6667131d62f63ff24b77aeb323701aa4fd2f03e9ae4560e71117b6bfb60798ec59298fb5a89c3fcb065433ec033df5b2177be2afd81c705bfc8df2bc06cd007917bfcbe4eddf9338b7714cee4867616d72ef03f3ec763503c2bf876c5e3796987e085c79c6f45de79c7eff432efa58ebc8c99dd8240d9b9d13a07110aadcd0374351f953903badabaef16f99cd998d896f965b1ab2597d6fa656c8a79cccdefb085d799d9e33ac2f30be4f136df4cafefc61b85c9ed73bedbb67111df5c1f35dfd12407d164d08ec52dda48613b652c515deedf455e00c969bee7f8c3f2f9512c899c00d43994cb51ba2c8e58d100ea1ae2fe04647f07f09214fec17d1cf2b9f2d3186245fd6113b7b8ef9ec58f467e9fe5eb325ef35cddd23f64322af3f8b15ab7813dfdd7b1a2ade37b55114d23e53414faaff6624b62ad14d1db28cc26586c3d8d2fda37165b2273b3a6aeaac8a67190acd5692ce16782d5c7b1c49da2fd6738cd3a4dcc7170d8ce596c6f428af5f6b09873dfd0f755ca5ce340e7cf277ec5f7b88f41f16cc519cef363503c5bf0d0322cdf350e22f3afaaddf823f3f1726fa98217a62fa6ff32d7301f29f2c5598e2bc7dfb44f36c7c779aea547b6cf93ef70116b588c042d3b3dc3cb0bdfb9cf6fe15a65e79481681a88e528d63ebada2473a70fc5b9a9b08326bf4d9703a0ad8fecfdf557d2ec01a665bc3b1238eb55b1afdac3de4107af53335ae78fc11daedab36affa4a18cedc14a5964ae2105d646af74cbcf7296ccdeadf819488e314320a8f65c4ae0c8236a19938ccd6f46a35deb1cc9c79a77673b473e20d3e837b455fb5f465bb5d7ae692be2199f7733cbcd4d882b9d9bc6a0801b83c12c6ff67f0e5e534705435d9d71bbd3833a2fc64ec527a241813773eb385eee05cee262759e39c9116731a88e89db27f2fff9d785c4c369531a68ceffbc6ef061a10e11dbbdb43e77865bd5e78623f1d3f29cfc2cdd8c07b763a9f7a5d7eff707d270f41f95e76efa62ffef2bcf71caff42754e1e8d245e1ee475b4dee8e646bcbd95466f55e7e4d1481cf56ad48ae7b7aa73efa3feb7d5b9ff9dbb524525edb4ca4759ef5fae177bd8f530c8c7ff7af76ec7f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad5f54ad53ff25ec75929a6b9cee168a317d3d843474381fe552c4aa0d3c58b69f48923cffe983f9361592aa38ebcf01d55f1dd0d248e7c08ef5525b13661ac7f15eb4f7db66142d05b86f778219a9b990824fea989d8f27a705f5c0f8979d9d0e8d7efe8aa2202bc464d797f4d81fca3992b42c7e7ea33ae360a6c9426d606b9f39522dada3a6b3ef7df4146973e3ddceaea530618bd81bf33a3d861eb6f5661f349d2381cad95b273b4a7b25cae1c5d5e261f514bdb1373b3eedf97b0aaec577df67c320e94d3118454d72cdf992ed0bcfce4e06823df52fdbd2d83f8f1a52add55d7652692a7fa6c4daa6b29f25621fcfe549790632bb8f35ff13b55124bfa98dff7e5567eeafce6fb409c50a7a08bea2a7899e5774945c32c3021d0526ce42df85a84df9f4538931176a2516ead8a4f000e6caec6e8da885a4d091c3139e81a8af4e942b4545e2acedde962636f9664ced7dac359fe80dfd71b8586ccd71eea2dbef5a978aeb7d8aa4a9cb8b0554e7ff1ee5bf6c5640ebd5c29ece099c0993aa08e3c09cfe43b3a9725a3eb714fea32b6a50e220673549ff9012d61d5a7dbeab3bde2687b4ed37cb5875e4f8cefefe2119b478d0eb119546bf37791375d1ae646273359f26da38fe7ab70fe5999b628eb79ffb4badef57f20af05bb6bc1ee5ab0bb16ecae05bb6bc1ee5ab0bb16ecae05bb6bc1ee5ab0bb16ecae05bbffd182ddafff030000ffff010000ffff99b91d64ce480000`))) +var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec7a5d93a2baf6f75739c5ede38c80e2b4de35cc8868b733ad3d229cda350501439a403804547ad77cf7a7125eb5dfdc67ef9bf32f2fba21c92259592fbfb5b2cc9f028a77840a933f0588b220773f0312f56920a6fe73df23809238f63336fc15a5c244e80724f2fb999b3a0e08fb0792861daa9e60440949b31f4e1608937727ec094b27f28589103928167ac25702848920f4844727856cc1b39520e9bb283e996045c84bba171cdd3b190884c9bf85cfc21f3d619d39d81726599afb5563e53b94c4c244a0acf52fcf4ffcd8f363504cfed5e11f924f00a3631f6024f4049d4c11f6299b95b1ff1912a1272421f43df6fa472d064ee0c780782886fd27b64c4fd8458cab37a7ee0c444e1aba4ee6d33e9b3a7d7790fd678b447e744a772e784678229f8f6853260eb63826906dedab9ff07db9f90eb16dbb45e653a127001225a94f697f879dccef76c06794f0769c3928f6d33e4634ab3afc237f4b8b2423cd4bdf29672c1b00250167a06a7bdd418f3a6dc307a74d4f561469fca2a38fe2cc4f6307f77defe0a41e3d27c3182519026d4f10399d56f379eac45e9e21fcca10cddd0cfbed40e4296d837dd7698161a7d1dd000d1ce9a4252ba393b622c99df6d99219eec8e9a888e3d3563f09d151e835f6d979ed3b3496ba6dd7a1fe6878d28362272dba3d80eebbcdc0ef4e5e1b7fd34eb89dfa694a52c6e50e3bf02ff906246ebedb3998f4033ff5cfc708c43efbf6e9352b7f31dc77e82524d08f3fa2da913472b2ecdc575f12b616584be032f2bfbe40e2a4f42f91d3cbb949520253e70c70028706089034617b3ba44ef2d630249fa21c67888be06f205fc34ee424f442d2cc097d12f75f250dfcc4f987a6e9ef10cefc73fdbe0edd17c1bb1fb9bef7df058237e868e69133fe12427d06d30cc0b19ff9ef8ff641e47d4cd147318baff81f0e4ee74bbf1fc13e22a4383f53034d76d2a09f54f0044912c2cf28ee174e843fef19f63273ae1e7d908201eb6bcc83854607c36e1748f26e73176594a459b72bf6b32c7580dfed23b47692d6f708176627169d7e92fa3bec830ca3eca49ba218627f87110c4e56a505050ec67dffe8033fdebf3694c73c5eb4c6efd38ce5043d81e70188f411a9a261d91db12cb07cf45d04dbd78cd6ef55248c50e4578f3ec784c4e142e11dffc949e67b498ae2cc7179802b6d83692dc8b2a4f3caffd5d26b3a6b8eab3e96732429e1f9066be7291be14e40281740f99697b89994bb600fe6ce7ed5aee4cbdfa07f4c9a973e2de2cc61924af3382b3756bdf5014f14eb96e7bbdcdeea7623592723114f3f5e8c54227dd14f0bc67e654a344b01e13aa4598a62c8878a18548f76fa4ab3424f6032e9677e9454b9db49bb8c21acb7dc4d1e2340bcce5b3fcf76d2e8b47dc39bd4d931babd1f7b24ed43829d187e2629ec1ffb75761738207064f132aa84e0421a88ca07d4fcc1bcef52ba3ae3798f384ff77e9d29be431784deee7d8a9749e23bc41fec9819b01753f617f9943af0ade94e5c04e6dc053fa44b52722c3e2094fb41e280f01d2ae4c5ce1bc3b4a01524be36caad90fa204ffdbe8b3c94e66f4aab34d8d489294b91de23aa6d944d78095dcce6fba3273cfa346bce7d718e71d9d59cf4caae7be23126277f0a179d81efd9e1b73a8d5e74bed6c93df13e20eb43f239221ea7def82945fc782b7d9686c2efdfbf7bc2aedcc3bb07fe097bfdc4e0bccf4fb818b96d2500c53bc29e9e9f3908f3a9e2f630dfa1ed09143dfbc264a00c473d2162a03119ca227ffdc5d16422c8a22c7e12bf7c92078fb23c91861349f93c504692389224e9ff89f24464d880e82f8fc967e760ea73e4e225097f2f4c46a38178d3138c980893c1cd97f18dcc9a4b8ce25098485c258c85817473d3137e224f9848a228f604bd7dddfefa95389e284cc49eb0f2d8a4624f587758577158ee64288e799380900a939b9e709ba188b1b2f681309194b1a20c1459517ac292b29ec197f170301e7c19feee09f71f90d69bfedd13b4cb49b7bf7ee5714e7d4f98fc5bec893df10fae647664bea868d328fabc7ad396683a142fcb346d21a62db694be50d55a2a959d165bba459492facc91ca3243eb67ff075cafca25188321bcb43cf6a61ffeee099e9339c244f00f041afa716fc9536ae80ff0c75a7db6cd6374774b1686761b1b051819fa430ef42c5edc126868b7d08da699fd28c6b606a8866ea1a1a912880ee3b9ec259e1e4816529e5c59dc03191e40b419ddc9abbd3558eedd48c19e360e5df93ef3b6abc46dbf27b6793cfcd81e6f76d51a86a6425b1f3fdd6d37a2631ea131530b776027b6be7936f46f701dd7f3a9891baf3088ecc0927f423b1a1786ae86aebc7c766525b7b72b78d7cc790badb5ba00f23877a38d687c9b3eacd78cf61edaf226b7b7f3c0d3c785315b116bfb00bded1283427df66673c91aac24106d42435f059efe0d3aa6f2ece99bc0d6d4bd8dd427579632db5444500c177cad59bd97906a9030fe034bce025bfe39fa8ed42fe5be6f634356b05700d617de4576bed0bcdcde02680de6d8daaed8fa81a1331af56099cbd4323d6ccc989c21b4652cdbe6105aba12b8e606de692a744ce9e00ee6a2a5a99b9fe1015aa6121a7a295f4b53457b1b88f3e2004df921b7b6f398ad679912667db6a6fcbc8b972288706e1721b3879a86e937bed3d4f00e3fe460b02a1c5389176bfe4d002240e67280ddd9ed4b9a8d98dc3576b3192c38af3fa931db14ae76808ebea1400bbb3281464933aa9eb4e9d7bcc4ddaa7b103fc4df11b7b9764ce7eb8faa676d5763bef64c2c75a0812766c7f3673232664becea560ef4e993234f637b432b5d789ea151b6afc83395a7bb98dbb4c1e4b7d03cbe074b5313ae97d99230593bfa34b73535720706b45a3de7252d28f73f5353fbb575d7cc4641c39f2bcfff639b4b71fe4c180f7b2b22642e711f483c1d337be0eb30dff04c09bbf12a7123d0f587bd1525d81aac9e0d2d807ea18e2d2dcce745c86552cffffd8940eefbda2d9c174bc6437c810d56733f50431f1f3c1defdd684add59083d198b8ea686f676f904225c8e213570a307684718bbfaeaf9cceeb84d7e607791636ea83dbb6fecaae6ffa55d71bd8fe705a0866e27b6790cc1b3f465b72e6dcbd0c7c889364fded776df35ee7c47e5b3c509b55e77d4cc551c2abf16e16e7d5834fe3be0f6a1d77c7664f8d3d17f426bd61d5f424397f6b68ed9fc85a18f657b3b2f1c93cb5864f4b5ac4abc533188578aa18f23439fe6863ea520da04f62c6c746337fa7e6032725b7facf9065c9e1b6e4b9c2ed9aec3da3f06ce764556d59a8b66af6732d3e8c898d1458da1358fdf5123a7c6dfb8afa1db4646f362c96c98c9a58c2b5135e7f4427f93e7ff79d7df1a1e991f79b06be7ccc681bca1366af6ce7db2896db22db951adb33172108c372c2ee958744c1667ec3d403c9648aeb9c93d9dd967277ee812f6f4606fcfeee14267732d45c71ce7a0e031a1f1c3c5bac2004e73ff2afe331dd9d190cc795b7d627af6aa58c778363af3df69ea1ecc56892b2b81ab1f95261ee89b676b304fcab16185fdd3c21e6caad804a831cdc2bb685ad8058c4bec6dec83c539862509f7b58a86f961c7860210cf037f8dbdc5baf2078e0b4119bfd780d9600022eff9c726f3168f0cef289ccb2def0cd72ef03f3ec777543eebfedd9ae3796587e08963ceb732eebce1774615f732575e25cc6e0152f75eb4294084437b7b0f3d3dc055cc809ebe197a653c67362676657e197675e4d259bfc2a684636e711bdbf126b7065c47f1e20279bcbe6fa6d737f1466572fb78df5ddbb868df5c1fcdbea36901a2a9d2c5e20e6fa4b49d0a4b348ffb771917407a1aef39fda87abe8725918b401343b91ca5cb70c48e1468e898fb1390833d8857a4f40feee390cf559c62881d0d472d6e71df3dc38f567e1fc5eb0aaf79aceee81f32195571fcb95eb7ed7bf8db58d1d5f19da68a9699711e4afdd7b9d88ad86b55f4b72ab30986ada7f8a27f63d81259db0df53455b6cca364af4fb1849f09d6ef638937c3878f68da755acc71e3b01bb3586e42caf50eb09cf3d0f2f755ca3df348178f277ec573dcefa87c767086eff93b2a9f9dfed036edc0338f22f3affabdf547e6e3556ea98127a62fa6ff2ad6301f29e3c5598cabc65fb54f36c7fb71aea34796e7c9b77189350c2341c74ecfe88a2a5741cb4a6f06b71f161f17da7ce3e9983adb55c230a13c3bb1b35af93dcbdf8d994a2d13e7c6b7a43a4f3d402f9a528fc5472d38800867ec7b2fda3c2fd64695ebda813bdbe06e8e69209e63a216afe61fe598d095ade68c53ebd91da8dcae2df348fd7599bf18fa0a037959385bb5b67926b7466f353f06ea62d9bcb477f45efe39e738fc014dbb0e6a70317f25469636569f75cb58d97cfbc35424573f241556d63e769277f178fd955467a643d3cf65f3486a19b5795ae313a4e3bf9d3cf6a13a1b95d82a5be6a1cdd3e2526666019e18a6cc25112e8a532cecc45f3297392ebc8bb1a5dd9c9f8becc0fddaf84eb5efee99e8666f4c1fa0258f734f9f266e7ccfdfc1601530ccace6e6b973e70cc4650d98ed4dbb18aad698333234af7e8f9bd8aa2b926bce317822702e57efa8a951240c8febd8e3c69bcc8a36458d098ea988b6e9f16fabbde49e29217b6b54fbb8855634debbfa2670f9fcf3bd2b1fe2fa7b3ed6ca01b9f298dae6346f796bcf2f73b9ce791bdecabcec918d9531795e9f57f4695ef5e7fe1a34bcf0fde94cdfb7c88d37d4d58c065f1df350edd3768d88d3a16d73e651397e7d47a7794cbd8ff35cef04f3eab3782c2ef8af62a91f676d49abad5bf17ad7bb05664c1cefd2baf2196d5d571e8dc5f7cbcaca27497e94be4c949b8934f83c180e878a341adffc3765e52f4371f8cf959539e77fa9aa2c0e87525dff55c663451e0dc7d2cbaaf2cd581e8fc5f1a02915d77b7e59557e8ff4ef5695af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701ff97ef239d9562da6b48ae3e7eb2cc0374758c8caf6259369e2d9f2c734878a9fa918caaf21375e565e06a6ae06d2171e56378a7a9a9bd0d13e3abd8964a4d0b82c12abc8b97a2b59d8b40e23f911247de2877e5b5a684ffec610e9b6f0c4d1541bcc1edcf521b0ae49fed5c117e7eacaf1fe863e4e02cb5b7d85bac55d1d137797b4de51632be8cd9f1c6d01e72c0f845c1de8a1297adbf5d876d89d63c3edb6b75efea0f6d8992ffbc33a6b67e20d67633bcabfaeaab0ff5cff50fe691723e50480ddd0edcd9122faa329dab8f035b0b0e8e0c92ef4f7509b1bee635957c2d606b5243cfb0bf0ee18f87e6a78fc446b7c18bfdced4d496dedfefdb72abca95df82008853ea967c5143034ff3e236ad7998230b023d8bcda2d3bf11e18f4711ce651cbbd1b8b0d7e54f572e6caf7419fa98daed4f3798c9c1d07164cc96a2ad29cf9e3e2dbcd972eb6c5764c1d73ac079711fbfad370a4d99af3d323afb3666e2b9de12bb299797b6caf92fbf7dcdbe98cca15fa8a51d3c1238d714eacad3f04cbee3735932bebe1f2a3d96fb8a589fab05cc0f68d5575f39a8af9ba8ae7ee03c2dd607e80fc4e4ee3619b379b4e89858a85e9b7f8bfdd9cab4b60699cb52e098c378b10e3f2cd396653dff5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bdffa5badeefff0f0000ffff010000ffffe2d72497d44b0000`)))