diff --git a/load.libsonnet b/load.libsonnet index 79dd84e..cdbc926 100644 --- a/load.libsonnet +++ b/load.libsonnet @@ -12,6 +12,8 @@ local lib = { local aux(old, key) = if !std.isObject(pkg[key]) then old + else if std.objectHasAll(pkg, '#' + key) && pkg['#' + key] == 'ignore' then + old else if std.startsWith(key, '#') then old { [key]: pkg[key] } else if self.scan(pkg[key]) then @@ -21,4 +23,5 @@ local lib = { std.foldl(aux, std.objectFieldsAll(pkg), {}), }; + lib.load(std.extVar('main')) diff --git a/main.go b/main.go index 51910b5..ea26f69 100644 --- a/main.go +++ b/main.go @@ -15,33 +15,52 @@ func main() { log.SetFlags(0) root := &cli.Command{ - Use: "docsonnet", + Use: "docsonnet ", Short: "Utility to parse and transform Jsonnet code that uses the docsonnet extension", + Args: cli.ArgsExact(1), } dir := root.Flags().StringP("output", "o", "docs", "directory to write the .md files to") - outputMd := root.Flags().Bool("md", true, "render as markdown files") 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") root.Run = func(cmd *cli.Command, args []string) error { file := args[0] - switch { - case *outputJSON: - model, err := docsonnet.Load(file) - if err != nil { - return err - } - data, err := json.MarshalIndent(model, "", " ") + log.Println("Extracting from Jsonnet") + data, err := docsonnet.Extract(file) + if err != nil { + log.Fatalln("Extracting:", err) + } + if *outputRaw { + fmt.Println(string(data)) + return nil + } + + log.Println("Transforming to docsonnet model") + pkg, err := docsonnet.Transform(data) + if err != nil { + log.Fatalln("Transforming:", err) + } + if *outputJSON { + data, err := json.MarshalIndent(pkg, "", " ") if err != nil { return err } - fmt.Println(string(data)) - case *outputMd: - return render.To(file, *dir) + return nil + } + + log.Println("Rendering markdown") + n, err := render.To(*pkg, *dir, render.Opts{ + URLPrefix: *urlPrefix, + }) + if err != nil { + log.Fatalln("Rendering:", err) } + log.Printf("Success! Rendered %v packages from '%s' to '%s'", n, file, *dir) return nil } diff --git a/pkg/docsonnet/fast.go b/pkg/docsonnet/fast.go index 2584fb6..d51d42a 100644 --- a/pkg/docsonnet/fast.go +++ b/pkg/docsonnet/fast.go @@ -11,7 +11,7 @@ import ( // Data assumptions: // - only map[string]interface{} and fields // - fields (#...) coming first -func fastLoad(d DS) Package { +func fastLoad(d ds) Package { pkg := d.Package() pkg.API = make(Fields) @@ -34,7 +34,7 @@ func fastLoad(d DS) Package { // non-docsonnet // subpackage? if _, ok := f["#"]; ok { - p := fastLoad(DS(f)) + p := fastLoad(ds(f)) pkg.Sub[p.Name] = p continue } @@ -94,7 +94,7 @@ func loadField(name string, field map[string]interface{}, parent map[string]inte return loadObj(name, iobj.(map[string]interface{}), parent) } - panic("field lacking {function | object}") + panic(fmt.Sprintf("field %s lacking {function | object}", name)) } func loadFn(name string, msi map[string]interface{}) Field { @@ -161,9 +161,9 @@ func loadObj(name string, msi map[string]interface{}, parent map[string]interfac return Field{Object: &obj} } -type DS map[string]interface{} +type ds map[string]interface{} -func (d DS) Package() Package { +func (d ds) Package() Package { hash, ok := d["#"] if !ok { log.Fatalln("Package declaration missing") diff --git a/pkg/docsonnet/load.go b/pkg/docsonnet/load.go index cf897f2..f2a9666 100644 --- a/pkg/docsonnet/load.go +++ b/pkg/docsonnet/load.go @@ -10,8 +10,22 @@ import ( "github.com/markbates/pkger" ) -// Load extracts docsonnet data from the given Jsonnet document +// 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) + if err != nil { + return nil, err + } + + return Transform([]byte(data)) +} + +// Extract parses the Jsonnet file at `filename`, extracting all docsonnet related +// 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) { // get load.libsonnet from embedded data file, err := pkger.Open("/load.libsonnet") if err != nil { @@ -33,15 +47,18 @@ func Load(filename string) (*Package, error) { // invoke load.libsonnet vm.ExtCode("main", fmt.Sprintf(`(import "%s")`, filename)) - log.Println("Evaluating Jsonnet") data, err := vm.EvaluateSnippet("load.libsonnet", string(load)) if err != nil { return nil, err } - log.Println("Transforming result") - // parse the result - var d DS + return []byte(data), nil +} + +// Transform converts the raw result of `Extract` to the actual docsonnet object +// model `*docsonnet.Package`. +func Transform(data []byte) (*Package, error) { + var d ds if err := json.Unmarshal([]byte(data), &d); err != nil { log.Fatalln(err) } diff --git a/pkg/render/fs.go b/pkg/render/fs.go index ef93c52..41247b9 100644 --- a/pkg/render/fs.go +++ b/pkg/render/fs.go @@ -2,31 +2,26 @@ package render import ( "io/ioutil" - "log" "os" "path/filepath" "github.com/sh0rez/docsonnet/pkg/docsonnet" ) -func To(api, out string) error { - pkg, err := docsonnet.Load(api) - if err != nil { - return err +func To(pkg docsonnet.Package, dir string, opts Opts) (int, error) { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return 0, err } - if err := os.MkdirAll(out, os.ModePerm); err != nil { - return err - } + data := Render(pkg, opts) - log.Println("Rendering .md files") - data := Render(*pkg) + n := 0 for k, v := range data { - if err := ioutil.WriteFile(filepath.Join(out, k), []byte(v), 0644); err != nil { - return err + if err := ioutil.WriteFile(filepath.Join(dir, k), []byte(v), 0644); err != nil { + return n, err } + n++ } - log.Printf("Success! Rendered %v packages from '%s' to '%s'", len(data), api, out) - return nil + return n, nil } diff --git a/pkg/render/render.go b/pkg/render/render.go index c337a74..0916a2c 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -2,6 +2,7 @@ package render import ( "fmt" + "path" "sort" "strings" @@ -10,14 +11,18 @@ import ( "github.com/sh0rez/docsonnet/pkg/slug" ) -func Render(pkg docsonnet.Package) map[string]string { - return render(pkg, nil, true) +type Opts struct { + URLPrefix string } -func render(pkg docsonnet.Package, parents []string, root bool) map[string]string { - link := "/" + strings.Join(append(parents, pkg.Name), "/") +func Render(pkg docsonnet.Package, opts Opts) map[string]string { + return render(pkg, nil, true, opts.URLPrefix) +} + +func render(pkg docsonnet.Package, parents []string, root bool, urlPrefix string) map[string]string { + link := path.Join("/", urlPrefix, strings.Join(append(parents, pkg.Name), "/")) if root { - link = "/" + link = path.Join("/", urlPrefix) } // head @@ -83,7 +88,7 @@ func render(pkg docsonnet.Package, parents []string, root bool) map[string]strin if root { path = parents } - got := render(s, path, false) + got := render(s, path, false, urlPrefix) for k, v := range got { out[k] = v } diff --git a/pkged.go b/pkged.go index 272cf02..f9d3729 100644 --- a/pkged.go +++ b/pkged.go @@ -9,4 +9,4 @@ import ( "github.com/markbates/pkger/pkging/mem" ) -var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec3b5b73a338d67f658bd7cfd3601c27b1abf621d0638c9db827761a0c5b535d42c28220108bc03699eafffe95c4c5d8b97967e67bf8b6fc90a0cbe1e8dc8f7490ff90c264439934fe43c2611e14de17486399054ae6bfc888424693c4cff9f4d73093c6921cd0d897732f030046f28e665107aa2799714ab3fc379007d2f843843d6901625f1a4b310813a9277da5501a4b524f7a0219e60b9eac84a9ec85c9118225a5afe15e51f400721848e37f495fa4df7bd22a07c497c61b40985ff7963e6034a91637e824243ee3e09cae2f984a3d298db08f78f3f7863f01b08939fa0e8f98fe0249b89721098f276290451ec87d267354d98793fc7f986039f6e363b8530972c023463f83cdfc0489c5432a87b4c84322f5244231e710e481bc0989cf1b9ccdaf7e2a78f48a4dc845e095b9cfa49e04699c663e63f28680dcef0ee0973015fd240761e2673209595e0ff87bd1caca34a76d430615c6aa03c33410c4d57dd49d440c1c3a3e3cee227538ec8f5e0dc86192fb590288eca31dc8103b0523244cf3101e468218747aedeb1948502dacd329567839f10f13311a1e3afcbd4e0f5e753a5d065800fa473d75787dd41ff6d54eff64c99c74e4b41f2aa3e39e9c46e15eea497e02290a13dc69ca8025fd6edf03ccbfbe3a1a09139095dd11c8b6dd6ee07791cbcfdc8f3afd54d8b09f6534e3546e08e0049cef37987ac566030895033ff34fe728263e7ff7f92d0f78352d03760e08f693cfa00e86d530761e780a3276eafc1f81671467e0240804800521a459ca17df65207d6f1ad35fe282e4a1a0f12f44a3969c18a4ec4cd01c443e4de43741033f057f131a1eb072ff54016f87d3b342ae1f7b3efa73c1f91d3896237a425f4a99cfc3230f9cc4cffd8f676518a3cf21e430613920e46f4e18a74b7f9c553e0364a43851034b37fd819cd66101d334c25fc2442e414cbe6c79cce3e65c3f6498c1011f6bcd83a72440707708a645b7bb897346b3bc3b94f8799e01e877c7286b9ce4e07b5408b393038e5fc9fc0df1614ec2fc6898850926fe868438385a95950c0242647fef433fd9be355524224e1f8cdf6779959b45fe7d9db2639ea9ab87ec85f8d0cc59d3ae33501cc67efd90454c4881108a18f87741731fa55998e4c01389a5b20daeb520cfd34e53fc6ba4d70e3614d7633cd7a71915799ef78b8ccf0827a04c08a06a1555284c2b2e8ef71f3da996af68617f9fb60d9995490eb8a4b222c92bc6ea960cc566ade921df13f6d6f45bc9829cc622edbf9aa945fa6a9c959cfcda94589e412a74c8f22c4cb0982a13583f0ee86bcd4a3d89cb44cefd38adf74c47fd2a2df0d18a9b220921459d965ce49bfef571ff567419d870b8ad9f209ac9981290e02f34c3f25e6e76550180015095f3a0524acafe40197e022d1edcfbce856b761a1f0117d9d66f76681fc00511da7c0cf17a73f601f0271c73034609e37fb1cf18c0efa13b72115c0817fc142ecde8befc0450958314c0e803a81025e09d6956b23a24be352bac90f9b0c87cd90b519815ef4aab32d80c246c43b3f823a0c64639c273e0128eeff79ef4e4b3bc3d7b250521d5507bdaaa861e28e2448eff90ce3a603ef093e538cf0abf77dee1d5a00f147d022663fa25a648405b7ec6427174ec7fe95f493f7ffeec499b8a870f4fd363defc858773599c3249e81d8ed9fc38ce9fc8cf414804aae47052eec0f62416bef8d258bd195df5a498078df195aa88e60f114dc692aaa8ca2fcaf097befad457c783db71ffe6cbadd2bfee8fd4e1f07f1475acf0d810b21f88cba73e15f378c7cffbfe561a5f5f0f949b9e6426541a0f6e6f46b7aa72db9316244c2269dc172af1a5f160d0bfbded49df43248dfb8aa2f424e3d05cfff89102a44863a5272d1147aaf4a45587748d44152757cae89a77298c9834beed497779187352563e94c6fde1ed48bd19dd8e6e7ad282f1915b9573d21f283f7bd2c327a00dd33f7b927e3ee8fac78f2229988fa4f1bf949ed2537e174ae647d5b32a22ada24f4b2387fa4707e2750de450e53854322a5f38b79051419f385275bc3ff8d97f81ebd57b094e6084cfad3dbdeb873f7b12023990c692bfa3d834f65b479d30d378c4bfadb417d7dec7f777746eea778959c26bd3782ca09127f33b8a4dfd0e7bf124779f94c4d521d3c33b6cea5a1fc6bbd14c45293282be130e9f3d55d94215ef606c5ddfabcbad33586cbd7848903e8a3cf52147eb65ea1ddea7aebddffdb6dedf6eea354c5dc3ae317abe5f5b0ab0f7d89c6aa5377053d7b05e4ce357bc4a1a7c5aea254b02633770d4efd88d47a5696891a72e5e3c7558b8eb25be6f71de6167a5cda13a2abcd852cc5f278fab15877dc0ae6a15ee7a162063549ad32575d68f18ad170496da0b9aceface60d987b11599c63240c6af18d8c317645881ab6b5b37d49e3db59fbbf65081e5d55cac356d7889988e29a73f70d43c70d5efd7df42eda6e2fb2e31d5214125e463d17dec16731d15ee1a62673023ce7ac9d70f4c83c3683bc75e648e8d8839e572c6d85589eada57d8318681675bf85ed730b0fb3b6f30531c5db3be473becd8c3c8342af93abaa6b8eb4099953b6cab8f85b39e257c3dc7ee133ee6eac3eff7c942813129dc32e2f6d0c070fd26f7ba16dd93c7020e9625b087c97c25de09600ce94c0d8837bd7b0d6329e97d6b37d6602e68fdcecca9557afa0e03c362508fba32c16605735d3f593baea3d45b6b5b983c26df426173873943ac7f5d3f1bbb1a89b5a74aa5031d3e733b9ebdd06b73ba209ee114d0983c037592b816ab758190a933ce578cece1f37d226cdae4f29beb48f0e0e85a2af4325d502e6b604c0a57d7626f6062e7a0e7a2828515ff532d73df5a77c56d14b6f479eaecdfaebd50662f94d3b075624a677de103293208b707b10ef70d64f789972c532f865d7fd83a714a9cc1f2c5d403ec97dac8d1a362564642260dfe6fcf140bdfd7eff0ac5c701a92336cb0c6fdc84c63b44306d97af18479d30823952840d72277bd788631a9e6422df0e247ecc68478c6f2e5c4ee844d7e627731b02de64e1f5abb6ae87f6d5742efa359099969b8a96bef23f8d2bfd9ac2adb328d510862eb197d3df0ddc49d6f61f53cc409ad59f7bac555ee6abf56f066b59bb7fe3b10f66134747664f81d18dfb133edce2fb069f4b7ae4138fed23446aabb9e95c0163256387c23ab2ade690426cba1698c62d39814a6316130b602771ab5ba715b7d3f721979077f6ce886429e96b0250197ae5751e31f03b05ed265bde6bce5f544663abb36a76cdec4d086c66f612ba7d6df84af8577ad8c66e582db30974b9557e21ae7e44c7f5367fffed0df5a1ab91f21dcb5736ee350b5981bb6bc0b9f6c739beaf6bdb8d1d92804214e2c9e970ca2009be719770b43914bfa9e6d15c8e0f6d9c91f469f2023d8bad3073c3738ae8502ec51014b91135a3f9cafea1820601ede8cff5c476e7c4567a2af3d733da33ad7719acd0efe7b5ddbc2e932f5d461e019fb619b0f0cebc519ccd26aeeaa8efd93d21d58756e82cc9ce4d17d3c29dd122755ec6ded83e7391e4b52e16b350cf7c38e0d05309905fe8aa0f9aaf6071117822a7faf20b7c100c6e8e5372b47f3271eef189ea907da795c3bc3ff048e6f61f56cc6372b11cf6b3b84cf22e6fc5ae59d77fcceacf35eeea9cb94db2d0cb52d8aad12c62472d70f181901a9730646867585aa7cce6d4ce9cafcbcd8d5914b67fd3a36a522e69677899b588533103a4ae667c8a3e6fb288fb9f155c7a7049d5e13c7d7abbb4cd8135fa71c0a1f5f095de3c431dce2637f12367a9cbb0cb7e8f852e5a73cd794ad8f733d553ea257b6ccfd6fae230b198481f532e5b2aef6668bd2b5270adf1b74e57b62a7f5ba87b13f99af04ad4d2e3ad0b8c3c898648286e992ba2b4dec81eef52a06a329d9b9ab2a7779034dd0e7d87be6af38de2571e349df9b3e62144f18b2bf9fc45db167f978af34580ea1f1098cdaaed3c239f670f8fe7e4ae0641dfa465e62314f8f8e6277b577a7d5b3b155fdaee2f989d6bc1fc65dc38a9db5c578de6cdb7a9b0bdb585fc5321e3360bbc7e1be52ef518e6db39e7fdb3e27a5abde61b07ee07bf517608c4a6404f53e42eced3b7a3c8ef97cbd8e9d9ec0357bacdbad3979c48e3a2a903149bde441b4e1601978c9b289f9c20e3a7b2ac55f6b0472fb9b3c76e2b7d6c4cc6b53474d3b69639431ec7bf68cc0678a676add0edb334fea258fadacbcc4ca9dd82a1bdd027ba8b83612efd6fc14c8ee87eedaacf9b8c34e3cda7a86157802ff6ceba9bba4795fcc7d6ddf0d3d75c45c7b521c683bec87666a93435bdaaa38ffc4e726258c27c359b3ff3126453d5ef82bd8d22274f9428f637df38e8e3a7e78b08f4aff1c86c7b774a4e37ffe5354cb333fc90f47ddc379569c833f2c3c110ad0b9f5a613d8a6de341c0d3f2d37294fea60ac8cc6839b2fb7ea70301a0c467fa6da7473a55cfd7dd52641f87f546cbab956af9ab2d0e046bdbabeb9bd55df2a36f5afae46379dba54c5b2f256b1e97dd0bf5a6cfaff73fda62a0c1d17ad18effd03f9a99f203f81e5f81fef5e11b8dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2b9dcd2f93fbb2a70520d39dc10f08cd1b363efb06790d0fcaa5455d8e9e2d9b1afa8a8f23ed1ebba6ac43c751178ba16a035a69eba8fee752d73d7516a7e55da8a22b01d0c07cbe83e5928ce7aa6c0bef87a41816a0defab1b07a9a8cada57ed3ba6ae2930b1c8e16bafc5a0fafd802b262f4fcd97416314029267ee9aa0f94a53806115872fc87798d3654ef7b7a6fe58404e6f186c9d38f5f8faeb5574a87cdafb1777a56d3de3f150ed135f6946cc3576d4595b57f7f558f355b2f992f668ef99a0238c9869b881375d9079fd45cc334681ab073ba0c2f4db73538d6b6e604cfabe1ef0359969e4c45f45f8b7c7f6eb40ea8677c12b7ea75ae6f63fe6f75db9fd673ae07461bfd42a593d510c8d3cb1d59cf8968237073a09e7d73448dc7eedaee4bc032a6ae42cbebc78f87033c33446ccd5b51b7352f1f22dbc8beee3fdd6b11f69679dd49c2a6feba8c655c9c7dd7ac692cd853c77f5585de5fcb5fecaa94c9857ebe15ed76e36ab88e98972a3e39401db2c6a3d553a5f078a154fcab98e72c726c5ac8cd273aa955579cbff6fab6f5d7e5e76295c5d0a5797c2d5a57075295c5d0a5797c2d5a57075295c5d0a5797c2d5a57075295cfd959f97fd2f000000ffff010000ffff9158754dad450000`))) +var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec3b5b93a2bad67fe514af9f7b04bc4c63d57968992da2ddceb4ce88706ad7544862a00d844340a577cd7fff2ae122dad7b3cf7efa3e1f5a929545b2ee2b59a4ff54c278cbb832fa53216116e4fe27c8a22e0fd4143f7511839cc531cec4f0973055464a376011ee667e0a00dc750f2cddb5b03a8a1d252ccdbe812c50466f4ed8511620c2ca488940182b1de50b83ca48513aca779012b1e0c54a8475fd303e9b60c9d873bc6714dd830c06cae85fca27e58f8eb2ca00c5ca284b735c75961870162b23858bde3f104e708c700c8bd13f5af413f61ba4e1b10b69a874148b4d428ab9985590ff8930a5a3243b829168fe518b4122e0183214c6a4fb2896e928db4850f5ead4ad8108a43b1f649877c5d4e99b83e2572c12e1e81cef52f002f14c3eefe1a6421c6271ca8860ed0b4e245f7ebe0d05db7e9161ae7414c8a224c59c77b71464b80d204f6122fb7106c218a75d1af2ac02e0a36ca54592b1a6d105e58c650786492009a8faa83d88383875303cef227d30d08c67806e1867388d01ed62740029e2976894864916c213248840abd7bc9e8218e559485f18e2b99f517c1a88d0e0d411efb57ab0dfeab419e001d0ce7afa6078d61f687aab7fb164465b723a0e54e3bcd74d76e151e934f6d96a76018fb576df071c0ffb6790300669d18640be6f7703dc9ebc36fea69f483bc569ca5241e59602f21ff906617ebedd02caba014ef1e51823148b771f5fb2f267c35dc03f824270fc1ed6c9b06ac63e869e80945f3af85be8292329b870f400f020842c4dc4e2871424af0d13f65b94d32c9434fe1711a721270209ff206a067698c5dd1751039c80bf699aee36a419be54c0cb21f3436115473e467f2d00bf82c733c42ee84b18c7223c8ac0497186df1eedc208bd8fd10d6391d7e8df9c142e977e3b73bc87c8697ea1069e6cb55e37a9c20261c98e7c0ae36e0122fa692f629e30e7ead18529ec0958631e2225014ada2098e4edee36ca384bb33628c659960288db30c66b2739f91e93c26ce580f35752bca5186634cccec03c8c09c55b1a92e06c555e700828ede2238638debf3494c7324e9f8c1ff34ce4e28e22f36fc8ba21abb250098ec4eeab7c74fd909c9a19afdb55068ac208578fae8c0909904291807fe72cc32849c33803be4c2ca56d08ad055996b49af2a7965e03ac29ae6022d7272993795ef4f3548c4827605c0aa06ce565284c4a2ec443b833aefa957c658be063d234babc8833202495e671563256b5ba506ed0ea1ec2beb4b7badf4816642c9269ffd94825d267705e08f22b53e2590a99d421cfd2302672a88861f5384d5f6956e9284226dd0c4749b5673aeb976941404b6ef238840cb55add3cdb6ac3f3fe8dec72b015787b1c23967609a320269f584abac76ebdab0a000c80ae7e0c2b61b4d07aeae01d6cf910def751bc7aa7f116729eee71bd437b032fd8a1eddb18cf37676f20bfc3b130601473f11761ce01796dba331721b974c177f192941d8b7710f56e9000b87b032b443178659817bc0a892f8d4a2be418e629eefa210ad3fc556995069b82986f591abd8554dba898f02378b198ef8f8ef21df3ac396fc539a525a8396195a07b860491a33f950f9d3defc5a1b33a057ee85c6bb17b86de41eb12f629624862af71ca4379acd43e697de5d7af5f1d655bf2f0e6417b249abf8970de95274b1afaa713b838a98b27c21908a99c2a3e1da25bb81d85874f5819e99f8d7e478944d018f57555367fca6832527455577f5307bf69fa77edf368301cf5079f54ad7f630cd57eef7f547da48ad810f29f48c8670b28c73272c95200de2ba3e1b0a77eee2876cc9451efe6b371a3ab371d6541c378a78c34a912ac8c7a3dede6a6a3fc089132d25455ed28d6a9b9f9f933014855466a47592231a9da51562dd2c7745772d2578da1e832b8e3cae8a6a3dc666124485961a88cb4c18da11b86aa0e3aca820b48ef46d70d7da8fdea28f76f63d62cffea28e68731373f7fe671ce315246ff523b6a47fd436a589c533f542969b47c593239d5455a18cf6b23a7eac7a9c2513a4255e0a8f4755ee168572e4aec0b2f2acff62727fb3fe077d5464210b8231fad49bdea84bf3a0a021950460a3e30625bc7bdab4fb86d3d906fabf193e71ca3bb5b36b7cddbd82ee0d0b61e726865f1fc9611dbbc257e34c9bcef6aec99909be12db1cdb106a38331d35182ac4073c3c1a3afab7ba893038cd6c33b7db9777b8bbd1f0d28328d9dafdf6768b34cfcd3fbcc738e876f9be3cdb65ac336c7c4b38cc7bbcd5a05ce91d8d371e1f7bcc4b3d64fb6f53b59c5f57ce3c48f9714465ee0ea3f881719856d8d77bebe78f2f541ee6d96e4ae99f396b8abf11cea46ee476bd5fe7df2b05a09dc7be2e9ebdcdbcc026419853d5d3277f340d0664161317e42d399e6f6961a8cd63bdb5a06c8fa9d0067f084ac75e099e3bd178e1f7d5dcb3c67a0c2a23f976b4d6b5e76dc244cd01fb87a1678fa8fe1d770fcb9e4fb36b6f501450514b0dd5de4e57313e5de0612b737a3ee6629d60f6c4be08c0faeb3485d07517b2ae44c88a753dd73fac4b50681efacc99d3926c0d10e7e6fa6bae678fd637720ae33d8d956295fd71cabde265067c58138fa43ee6e66b158cf75342a609e39f871172f5418d1dc2b76c21e6a1ca1dff8ce1cefeee8430e7bcb02388378be92ef0430826ca607d49fde3ec759abc95d6337ebde5cd2fa83dbd375e19b0702ac3587e6ae2d13629738c3eac91bb889127f33dec3f821fe1a4a9b3b8d5972fd61f5acedca906b4fd55207267c14763c7b62437bbaa0bee5e6d09a3c027d127b6b5ee90221dbe482af083983c7bb58dab42de4373791e4c135c789d4cb74c184ac8135c93d731cf93d9bb8273de7252e2cf99f8e53efa57557c24661439fafcffeed390b75f6c4040d7b37626ca6491f489045853dc875846f2047a37ebc4cfc08b6fd61ef4609757bcb27db0c082ec6866beef259b19332a9e7fffac888f47df396cc8a85a021fe800d56733f70db320ec8a27b3f9a707fba2348a72a30c73b6fb37884112dc7c271e0470fc48b28f5ade5d385dd499b7cc7ee22e0acb937bd6fecaaa6ffb95d49bd1bb30272dbf212cf39eee093f679bb2a6dcbb68c1044eb47f4e5c4771d77be86e5f31427c6f5bac366aee250f9b54ab6abc3bcf1df9eb40faba6b325c31fc0fa41dc697b7c416c4bdb7b1615f317b665e8de66560047ca5815f8b5acca7837a6305e0e6ccb886c6b92dbd684c3681d78d35da31bafd1f78390917ff2c79a6e28e5b996b624f192cd6a57fb470f6c966c59ad396f78bd9099c987f694cfeb185ad3f8356ce4d4f89bf4b5f0b691d1ac58081b167229f34a54cd39f9a0bfe9b37fbfe96f0d8dc28f1069dbb9b071a8afb91736bc4b9f6c729bee697e54ebcc084148e2b5c84b16558123f28cb787a1cc259aefac736409fb6ce50f4ba3c80af6def49ecc2d31d742058e91c342e684c60fe7ab2a06489cfb17e3bfd09117f5d94cf6c78f42cfa8ca758266bb35ff9d39dec3e932f1f541e05bc741930facf593db9b25e558bf8afd93c2ebadabdc04b93dc97677d1a4f00a1297b1b7b10f91e7442c49a4af5538c20f5b3614c07816e01545f355e50f322e0465fe5e416183018cd0d3b77586e6df45bce364a69f681771ed03fe27e7f81a96cf1abe5dc9785ed9217c9431e7f732efbce2777695f7325f5f26c26e6138dea3685dc088eebccd3d415640ab9c4190b5eea3329f0b1b53db32ff58ec6ac9a5b57e159b1219738bdbd88bd7b9db933a8ae71f9047c5f7591ef3a27ecba7249d7e1dc737abdb54da9358a718481f5f495d93d8b5bcfc6d7f92367a9ebb2c2f6ff952e9a722d7148d8f0b3d953e6296b62cfc6f6ea235b228079b6522645deecd1685e74c54b13768cbf7c24eab754fb0bf98af24ad752e3ad17820c89aa49286e99279abb1dc03dd99650c46537af05665eef27b63499feb1c395e897997d48b269a3f7d20289a70e4fcb888bb72cff2f65ea9b71c40eb1d1cbd59a7c1739dc1e0f5fd949c93b7e833fc78cd7d737716bbcbbd3b2b9fb5ad9ab725cfdf59c5fb09ee59ebc8ddacb9c89b4ddb6c726113ebcb582662066cf638c257aa3dcab96d56e32fdbe7a4f0f45b0236f762affe042ca3405650ed23e4debea5c7f3982fd66bd9e9055ebdc7bad9db9307e2ea468eac49e2c7f7b20d7bcbc08f9775cc9776d0da53a97833a650d8dfe4a115bfc775cc1cda26aadb7113a3ac81e63b330a1f1999e9553b6cce3c891f3f34b2f2e375e646eba2d62d7006aae720f96ec54f8e1c2df43676c5c72d712363ef5bebc097f3cff6be7e88ebf7e5d897e6ddd0d70dee3993fc44db693f34d3eb1cdad056c6f9ef626c52c0683298d5fb1f6b9257f01caf60438bd4e5133b8ff5f53b266af9e1c93e4afd0b1c11df12c324fffca72c95a738ce4e47ddd379569e83dfac3a5106d0478b4d17b875b16968a81fac35dd8cb4dea75ebfdf1f6843e3e6afd49a3ef7d5fedf576b9294ff67a5264dd6baca5293f1f9b37a73a319afd59a8c5e835af3fc5ab1e965d4ffb6da74bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd9773bd97f3fffd5ece5fbb1f705102395d0bf02de3d1750ec4b768687f51cbd2eb74f1e83a7d264bbbdfd9b02a15715f5f04be390ed086305f3feeeecc71ea6d7689fd456dca88c07109ec2d7777f142753733156af2930503fa7a70575e33486429d6e937efd8e65885f19a9e3ef1ae39d47f9ce68ae8d3f7fa73a065848066a9b7a168be1aabc05ae7a7cfc6b744d0654f8f37b6f99043416f18ecdd28f1c5fa9bd5ee54ee748e4fde6abcf7ad8753894f7e9a31b8671d98bb59f7ef2a58fd29b2fe7cf6e01cb9a423dc71dbf2027fbaa0f3ea33986f1981670607a0c3e4eb635d82abaf5d4c346c06624d6e5b19c5ab1df9f6d07c1248bcf03678c6ef749c7adadbfcbe2eb7aadcf77b104075c2fd922e6e9bf07156dca6350db3d025d0ca62a768c1d72af9f65d25339dc67e6414de0aca4f283e395db1b02d837ba74f3954c8c1b668644f17aa670e9e903529d074b1019b259bcbb50e6456dcc7afeb8d1347976b0fed16dff654bdd45be2d59f44e2d25625fde5bb2fd9979039c1c5b8b483ef8ccccc01f7f5c9ee42bec6a52c055d5f0f951e4bbe2201f3cd40f801af60f527c0faf3efd8b70e92a6f9ea40704f4dee6e1343cc6346c7c40debb5e5bb144f978ebbb1d94cd702e0f4e3f96a377faf3c5a96d3f0b59e76ada75deb69d77adab59e76ada75deb69d77adab59e76ada75deb69d77adab59e76ada75deb691ffa3fb7ff050000ffff010000ffffdce249f251460000`)))