diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5e6cea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cparse +docsonnet diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2623f2b --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.PHONY: docsonnet cparse + +VERSION := $(shell git describe --tags --dirty --always) + +docsonnet: pkged.go + go build -o docsonnet -ldflags "-X main.Version=dev-$(VERSION)" . + +cparse: pkged.go + go build -o cparse -ldflags "-X main.Version=dev-$(VERSION)" ./cmd/cparse + +pkged.go: doc-util/main.libsonnet pkg/comments/dsl.libsonnet + rm -f cmd/cparse/pkged.go + pkger + ln -s $(PWD)/pkged.go cmd/cparse/pkged.go diff --git a/main.go b/main.go index ac3c308..3a63f5c 100644 --- a/main.go +++ b/main.go @@ -11,13 +11,16 @@ import ( "github.com/sh0rez/docsonnet/pkg/render" ) +var Version = "dev" + func main() { log.SetFlags(0) root := &cli.Command{ - Use: "docsonnet ", - Short: "Utility to parse and transform Jsonnet code that uses the docsonnet extension", - Args: cli.ArgsExact(1), + Use: "docsonnet ", + Short: "Utility to parse and transform Jsonnet code that uses the docsonnet extension", + Args: cli.ArgsExact(1), + Version: Version, } dir := root.Flags().StringP("output", "o", "docs", "directory to write the .md files to") diff --git a/pkg/comments/comments.go b/pkg/comments/comments.go new file mode 100644 index 0000000..8a90f5a --- /dev/null +++ b/pkg/comments/comments.go @@ -0,0 +1,145 @@ +package comments + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/google/go-jsonnet" + "github.com/google/go-jsonnet/formatter" + "github.com/markbates/pkger" +) + +type Blocks []string + +func (b Blocks) String() string { + return strings.Join(b, "---\n") +} + +func Transform(filename, data string) (string, error) { + return TransformStaged(filename, data, StageFormat) +} + +type Stage int + +const ( + StageScan Stage = iota + StageTranslate + StageEval + StageJoin + StageFormat +) + +func TransformStaged(filename, data string, stage Stage) (string, error) { + // Stage 0: Scan for comments + blocks, err := Scan(data) + if err != nil { + return "", err + } + if stage == StageScan { + return blocks.String(), nil + } + + // Stage 1: Translate to DSL + for i := range blocks { + blocks[i] = Translate(blocks[i]) + } + if stage == StageTranslate { + return blocks.String(), nil + } + + // Stage 2: Eval DSL + for i := range blocks { + blocks[i], err = Eval(blocks[i]) + if err != nil { + return "", err + } + } + if stage == StageEval { + return blocks.String(), nil + } + + // Stage 3: Join + joined := data + Join(blocks) + if stage == StageJoin { + return joined, nil + } + + // Stage 4: Format + formatted, err := formatter.Format(filename, joined, formatter.DefaultOptions()) + if err != nil { + return "", err + } + return formatted, nil +} + +// Scan extracts comment blocks from the Jsonnet document +func Scan(doc string) (Blocks, error) { + doc, err := formatter.Format("", doc, formatter.Options{ + CommentStyle: formatter.CommentStyleHash, + }) + if err != nil { + return nil, err + } + + var blocks []string + block := "" + + for _, l := range strings.Split(doc, "\n") { + l := strings.TrimSpace(l) + if !strings.HasPrefix(l, "#") { + if block != "" { + blocks = append(blocks, block) + block = "" + } + continue + } + + block += l + "\n" + } + + return blocks, nil +} + +// Translate converts the comment syntax into Jsonnet DSL invocations +func Translate(block string) string { + block = strings.Replace(block, "# @", "+ ", -1) + block = strings.Replace(block, "# ", "", -1) + return block +} + +// Eval converts the block into an actual object, by evaluating it in the +// context of our DSL +func Eval(block string) (string, error) { + dsl := loadDSL() + + vm := jsonnet.MakeVM() + out, err := vm.EvaluateSnippet("", dsl+"\n"+block) + if err != nil { + return "", err + } + + return out, nil +} + +// Join chains multiple comment blocks into a single patch +func Join(blocks Blocks) string { + s := "" + for _, b := range blocks { + s += "+ " + b + } + return s +} + +func loadDSL() string { + p, err := pkger.Open("/pkg/comments/dsl.libsonnet") + if err != nil { + // This must work. panic if not + panic(fmt.Errorf("Loading embedded file: %s. This build appears broken", err)) + } + data, err := ioutil.ReadAll(p) + if err != nil { + panic(fmt.Errorf("Loading embedded file: %s. This build appears broken", err)) + } + return string(data) +} diff --git a/pkg/comments/dsl.libsonnet b/pkg/comments/dsl.libsonnet new file mode 100644 index 0000000..96c9fd2 --- /dev/null +++ b/pkg/comments/dsl.libsonnet @@ -0,0 +1,52 @@ +local d = import 'doc-util/main.libsonnet'; + +local pkg(name, url) = { + NAME:: { + TYPE:: { help: "" }, + name: name, + 'import': url, + help: self.TYPE.help, + }, + "#": self.NAME, +}; + +local dType(kind) = function(name) { + // NAME is used to mix into the docsonnet field without knowing it's name + NAME:: { + // TYPE is used to mix into {function,object,value} without knowing what it is + TYPE:: {}, + [kind]: self.TYPE, + }, + ['#' + name]: self.NAME, +}; + +local fn = dType('function'), + obj = dType('object'); + +local val(name, type, default=null) = dType('value') + { + NAME+: { TYPE+: { + type: type, + default: default, + } }, +}; + +local arg(name, type, default=null) = { + NAME+: { TYPE+: { + args+: [{ + name: name, + type: type, + default: default, + }], + } }, +}; + +local desc(help) = { + NAME+: { TYPE+: { + help: help, + } }, +}; + +local string = 'string', + object = 'object'; + +{} diff --git a/pkged.go b/pkged.go index 29957fb..f752ae6 100644 --- a/pkged.go +++ b/pkged.go @@ -9,4 +9,4 @@ import ( "github.com/markbates/pkger/pkging/mem" ) -var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec7a5d93a2baf6f75739c5ede38c80e2b4de35cc8868b733ad3d229cda350501439a403804547ad77cf7a7125eb5dfdc67ef9bf32f2fba21c92259592fbfb5b2cc9f028a77840a933f0588b220773f0312f56920a6fe73df23809238f63336fc15a5c244e80724f2fb999b3a0e08fb0792861daa9e60440949b31f4e1608937727ec094b27f28589103928167ac25702848920f4844727856cc1b39520e9bb283e996045c84bba171cdd3b190884c9bf85cfc21f3d619d39d81726599afb5563e53b94c4c244a0acf52fcf4ffcd8f363504cfed5e11f924f00a3631f6024f4049d4c11f6299b95b1ff1912a1272421f43df6fa472d064ee0c780782886fd27b64c4fd8458cab37a7ee0c444e1aba4ee6d33e9b3a7d7790fd678b447e744a772e784678229f8f6853260eb63826906dedab9ff07db9f90eb16dbb45e653a127001225a94f697f879dccef76c06794f0769c3928f6d33e4634ab3afc237f4b8b2423cd4bdf29672c1b00250167a06a7bdd418f3a6dc307a74d4f561469fca2a38fe2cc4f6307f77defe0a41e3d27c3182519026d4f10399d56f379eac45e9e21fcca10cddd0cfbed40e4296d837dd7698161a7d1dd000d1ce9a4252ba393b622c99df6d99219eec8e9a888e3d3563f09d151e835f6d979ed3b3496ba6dd7a1fe6878d28362272dba3d80eebbcdc0ef4e5e1b7fd34eb89dfa694a52c6e50e3bf02ff906246ebedb3998f4033ff5cfc708c43efbf6e9352b7f31dc77e82524d08f3fa2da913472b2ecdc575f12b616584be032f2bfbe40e2a4f42f91d3cbb949520253e70c70028706089034617b3ba44ef2d630249fa21c67888be06f205fc34ee424f442d2cc097d12f75f250dfcc4f987a6e9ef10cefc73fdbe0edd17c1bb1fb9bef7df058237e868e69133fe12427d06d30cc0b19ff9ef8ff641e47d4cd147318baff81f0e4ee74bbf1fc13e22a4383f53034d76d2a09f54f0044912c2cf28ee174e843fef19f63273ae1e7d908201eb6bcc83854607c36e1748f26e73176594a459b72bf6b32c7580dfed23b47692d6f708176627169d7e92fa3bec830ca3eca49ba218627f87110c4e56a505050ec67dffe8033fdebf3694c73c5eb4c6efd38ce5043d81e70188f411a9a261d91db12cb07cf45d04dbd78cd6ef55248c50e4578f3ec784c4e142e11dffc949e67b498ae2cc7179802b6d83692dc8b2a4f3caffd5d26b3a6b8eab3e96732429e1f9066be7291be14e40281740f99697b89994bb600fe6ce7ed5aee4cbdfa07f4c9a973e2de2cc61924af3382b3756bdf5014f14eb96e7bbdcdeea7623592723114f3f5e8c54227dd14f0bc67e654a344b01e13aa4598a62c8878a18548f76fa4ab3424f6032e9677e9454b9db49bb8c21acb7dc4d1e2340bcce5b3fcf76d2e8b47dc39bd4d931babd1f7b24ed43829d187e2629ec1ffb75761738207064f132aa84e0421a88ca07d4fcc1bcef52ba3ae3798f384ff77e9d29be431784deee7d8a9749e23bc41fec9819b01753f617f9943af0ade94e5c04e6dc053fa44b52722c3e2094fb41e280f01d2ae4c5ce1bc3b4a01524be36caad90fa204ffdbe8b3c94e66f4aab34d8d489294b91de23aa6d944d78095dcce6fba3273cfa346bce7d718e71d9d59cf4caae7be23126277f0a179d81efd9e1b73a8d5e74bed6c93df13e20eb43f239221ea7def82945fc782b7d9686c2efdfbf7bc2aedcc3bb07fe097bfdc4e0bccf4fb818b96d2500c53bc29e9e9f3908f3a9e2f630dfa1ed09143dfbc264a00c473d2162a03119ca227ffdc5d16422c8a22c7e12bf7c92078fb23c91861349f93c504692389224e9ff89f24464d880e82f8fc967e760ea73e4e225097f2f4c46a38178d3138c980893c1cd97f18dcc9a4b8ce25098485c258c85817473d3137e224f9848a228f604bd7dddfefa95389e284cc49eb0f2d8a4624f587758577158ee64288e799380900a939b9e709ba188b1b2f681309194b1a20c1459517ac292b29ec197f170301e7c19feee09f71f90d69bfedd13b4cb49b7bf7ee5714e7d4f98fc5bec893df10fae647664bea868d328fabc7ad396683a142fcb346d21a62db694be50d55a2a959d165bba459492facc91ca3243eb67ff075cafca25188321bcb43cf6a61ffeee099e9339c244f00f041afa716fc9536ae80ff0c75a7db6cd6374774b1686761b1b051819fa430ef42c5edc126868b7d08da699fd28c6b606a8866ea1a1a912880ee3b9ec259e1e4816529e5c59dc03191e40b419ddc9abbd3558eedd48c19e360e5df93ef3b6abc46dbf27b6793cfcd81e6f76d51a86a6425b1f3fdd6d37a2631ea131530b776027b6be7936f46f701dd7f3a9891baf3088ecc0927f423b1a1786ae86aebc7c766525b7b72b78d7cc790badb5ba00f23877a38d687c9b3eacd78cf61edaf226b7b7f3c0d3c785315b116bfb00bded1283427df66673c91aac24106d42435f059efe0d3aa6f2ece99bc0d6d4bd8dd427579632db5444500c177cad59bd97906a9030fe034bce025bfe39fa8ed42fe5be6f634356b05700d617de4576bed0bcdcde02680de6d8daaed8fa81a1331af56099cbd4323d6ccc989c21b4652cdbe6105aba12b8e606de692a744ce9e00ee6a2a5a99b9fe1015aa6121a7a295f4b53457b1b88f3e2004df921b7b6f398ad679912667db6a6fcbc8b972288706e1721b3879a86e937bed3d4f00e3fe460b02a1c5389176bfe4d002240e67280ddd9ed4b9a8d98dc3576b3192c38af3fa931db14ae76808ebea1400bbb3281464933aa9eb4e9d7bcc4ddaa7b103fc4df11b7b9764ce7eb8faa676d5763bef64c2c75a0812766c7f3673232664becea560ef4e993234f637b432b5d789ea151b6afc83395a7bb98dbb4c1e4b7d03cbe074b5313ae97d99230593bfa34b73535720706b45a3de7252d28f73f5353fbb575d7cc4641c39f2bcfff639b4b71fe4c180f7b2b22642e711f483c1d337be0eb30dff04c09bbf12a7123d0f587bd1525d81aac9e0d2d807ea18e2d2dcce745c86552cffffd8940eefbda2d9c174bc6437c810d56733f50431f1f3c1defdd684add59083d198b8ea686f676f904225c8e213570a307684718bbfaeaf9cceeb84d7e607791636ea83dbb6fecaae6ffa55d71bd8fe705a0866e27b6790cc1b3f465b72e6dcbd0c7c889364fded776df35ee7c47e5b3c509b55e77d4cc551c2abf16e16e7d5834fe3be0f6a1d77c7664f8d3d17f426bd61d5f424397f6b68ed9fc85a18f657b3b2f1c93cb5864f4b5ac4abc533188578aa18f23439fe6863ea520da04f62c6c746337fa7e6032725b7facf9065c9e1b6e4b9c2ed9aec3da3f06ce764556d59a8b66af6732d3e8c898d1458da1358fdf5123a7c6dfb8afa1db4646f362c96c98c9a58c2b5135e7f4427f93e7ff79d7df1a1e991f79b06be7ccc681bca1366af6ce7db2896db22db951adb33172108c372c2ee958744c1667ec3d403c9648aeb9c93d9dd967277ee812f6f4606fcfeee14267732d45c71ce7a0e031a1f1c3c5bac2004e73ff2afe331dd9d190cc795b7d627af6aa58c778363af3df69ea1ecc56892b2b81ab1f95261ee89b676b304fcab16185fdd3c21e6caad804a831cdc2bb685ad8058c4bec6dec83c539862509f7b58a86f961c7860210cf037f8dbdc5baf2078e0b4119bfd780d9600022eff9c726f3168f0cef289ccb2def0cd72ef03f3ec777543eebfedd9ae3796587e08963ceb732eebce1774615f732575e25cc6e0152f75eb4294084437b7b0f3d3dc055cc809ebe197a653c67362676657e197675e4d259bfc2a684636e711bdbf126b7065c47f1e20279bcbe6fa6d737f1466572fb78df5ddbb868df5c1fcdbea36901a2a9d2c5e20e6fa4b49d0a4b348ffb771917407a1aef39fda87abe8725918b401343b91ca5cb70c48e1468e898fb1390833d8857a4f40feee390cf559c62881d0d472d6e71df3dc38f567e1fc5eb0aaf79aceee81f32195571fcb95eb7ed7bf8db58d1d5f19da68a9699711e4afdd7b9d88ad86b55f4b72ab30986ada7f8a27f63d81259db0df53455b6cca364af4fb1849f09d6ef638937c3878f68da755acc71e3b01bb3586e42caf50eb09cf3d0f2f755ca3df348178f277ec573dcefa87c767086eff93b2a9f9dfed036edc0338f22f3affabdf547e6e3556ea98127a62fa6ff2ad6301f29e3c5598cabc65fb54f36c7fb71aea34796e7c9b77189350c2341c74ecfe88a2a5741cb4a6f06b71f161f17da7ce3e9983adb55c230a13c3bb1b35af93dcbdf8d994a2d13e7c6b7a43a4f3d402f9a528fc5472d38800867ec7b2fda3c2fd64695ebda813bdbe06e8e69209e63a216afe61fe598d095ade68c53ebd91da8dcae2df348fd7599bf18fa0a037959385bb5b67926b7466f353f06ea62d9bcb477f45efe39e738fc014dbb0e6a70317f25469636569f75cb58d97cfbc35424573f241556d63e769277f178fd955467a643d3cf65f3486a19b5795ae313a4e3bf9d3cf6a13a1b95d82a5be6a1cdd3e2526666019e18a6cc25112e8a532cecc45f3297392ebc8bb1a5dd9c9f8becc0fddaf84eb5efee99e8666f4c1fa0258f734f9f266e7ccfdfc1601530ccace6e6b973e70cc4650d98ed4dbb18aad698333234af7e8f9bd8aa2b926bce317822702e57efa8a951240c8febd8e3c69bcc8a36458d098ea988b6e9f16fabbde49e29217b6b54fbb8855634debbfa2670f9fcf3bd2b1fe2fa7b3ed6ca01b9f298dae6346f796bcf2f73b9ce791bdecabcec918d9531795e9f57f4695ef5e7fe1a34bcf0fde94cdfb7c88d37d4d58c065f1df350edd3768d88d3a16d73e651397e7d47a7794cbd8ff35cef04f3eab3782c2ef8af62a91f676d49abad5bf17ad7bb05664c1cefd2baf2196d5d571e8dc5f7cbcaca27497e94be4c949b8934f83c180e878a341adffc3765e52f4371f8cf959539e77fa9aa2c0e87525dff55c663451e0dc7d2cbaaf2cd581e8fc5f1a02915d77b7e59557e8ff4ef5695af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701af5701ff97ef239d9562da6b48ae3e7eb2cc0374758c8caf6259369e2d9f2c734878a9fa918caaf21375e565e06a6ae06d2171e56378a7a9a9bd0d13e3abd8964a4d0b82c12abc8b97a2b59d8b40e23f911247de2877e5b5a684ffec610e9b6f0c4d1541bcc1edcf521b0ae49fed5c117e7eacaf1fe863e4e02cb5b7d85bac55d1d137797b4de51632be8cd9f1c6d01e72c0f845c1de8a1297adbf5d876d89d63c3edb6b75efea0f6d8992ffbc33a6b67e20d67633bcabfaeaab0ff5cff50fe691723e50480ddd0edcd9122faa329dab8f035b0b0e8e0c92ef4f7509b1bee635957c2d606b5243cfb0bf0ee18f87e6a78fc446b7c18bfdced4d496dedfefdb72abca95df82008853ea967c5143034ff3e236ad7998230b023d8bcda2d3bf11e18f4711ce651cbbd1b8b0d7e54f572e6caf7419fa98daed4f3798c9c1d07164cc96a2ad29cf9e3e2dbcd972eb6c5764c1d73ac079711fbfad370a4d99af3d323afb3666e2b9de12bb299797b6caf92fbf7dcdbe98cca15fa8a51d3c1238d714eacad3f04cbee3735932bebe1f2a3d96fb8a589fab05cc0f68d5575f39a8af9ba8ae7ee03c2dd607e80fc4e4ee3619b379b4e89858a85e9b7f8bfdd9cab4b60699cb52e098c378b10e3f2cd396653dff5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bd6b5def5ad7bbd6f5ae75bdffa5badeefff0f0000ffff010000ffffe2d72497d44b0000`))) +var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec3c5b73a338977fa58bd775b7015f12fc16e836c64e9c8e9d189bafa6a640c282201083c03699eaffbe2571b573ede9d9dadd193f7440d2413af7231d1ff59f821f6d0915467f0ac84fbdccf90248d8a59e98b84f5d48002551e4a66cf8ab9f0823a1eb91d0eda64e62db20e8ee4912b4a03a8211c62449bfdba9278cde9cb023ccedd015464268fb91d011be12208c04a123dcdb09620b9eac8448d7f1a3a30916843c877b86d18d9d024f18fd47f822fcd61196a98d5d619426995b3616ae4d49248c8488a49ffc88a636c62efce464e9277b67fbd876b0fbc98f3e39998fe1276003cf153a824ec63e76299b9711f00511a123c40172217bfdad620407702340a01fa1ee235ba8236c4386578b39887c06d83f7401f68f07423b091c3b7569974d9dbc39c8feb24542373c863b653d033ce2d07bb0891b41be38268891f6d58d395d4eb6f519d94e9eba54e808808471e252dadd623b75db1de8c98f793b4a6d3f72932ef6695a76b807fe96e4714aea97ae5dcc5834801f7b1c81b20ddb8390da4dc305c74d280f0692f2aca3eb47a99b4436eeba706f27909e8261ecc7a90f9a1e2fb45badfaf3c48e6096faf885219a3929769b81100e9a06fbaed502fd56a34d00f56ce9a8250f8647ed8124b7da274ba6b8c5a7c340548e5bdd38f00f42a7d6cfd66bd7a691d46e3b367587fda31e3fb293bcdd03e8aeddf4dcf6e495f2d7ed98eba99b242461586eb18d7eca361071b2edd6c6a4ebb9897b3a461076d9b78f2f69f9b3e1ae4d3f0282dce83da82d49423b4d4f6df53960a38115073e06fef30bc476427f0a9c7e1c9b382128b14f1c8e6753cf072489196dfbc48e5f1b46e47398e1d4e72cf805cf57a313da31fd20686a072e89ba2f827a6e6cff4dd374b73e4edd53f9beecba3fe4deddd071e15f0b04afc0d1149213fc62425de6a69903c76eeabe3dda05217c1fa25bc6d6bf39389d2efd76047b0f90e2ec440c34de4abd6e5cba2744e2007df1a36e6e87f8cb8ef95ea6cee5a30b12d0637db57ab0d06863d4ee0271d66e6ec39492246d77456e9a263670db7d845646d2d81ee1cc6cc5a2e34f12778b5d90623f3deaa67e84b0bbc53ef28e56a5390536c65df7e00237dabd3494453c5e34caefd294ed093a02df07f8a4eb93321a16dd21db07168faee3a3e635a5d57b1909433f74cb4797fb84d8e64ce11d7f642475619cf851ca7663424728748349cd4bd3b8f5caff54dcab3b2b8ccb3eb6e78813c2f71bac9d256c841b01a19c01c55b56f8cdb8a0823d9839bb65bbe42f7f43ee21ae5fba348f529b712ac9a2b420ac7ceb02be51ac5ad075b8be55ed9ab3764a42befd783652b2f4593fcd19faa52ad1340184cb90a6891f213e9447a07c34d39792153a02e3493775c3b8dcbb1db58b18c27a0b6ab2c80704b6deba59ba9586c7ed4bdea4f696c1eddc0892a48b08b623f48524a87be856bb3bcf069e2d8b1f838a09cea59e3878079a3f98f57d14aedaf1bc059c253bb7da29be01e70570fb36c4f34de21bc0ef50cc14184694fd0b5d4a6df4da74472682326e82efc2c50939e4ef00ca5d2fb641f006940f23fb95619ad3d225be34cab590ba204bdcaee3433fc95ee556a1b0891d51b6457a0ba8d25136e147e02236df6f1de1dea5697dee8b328c8baefaa45774dd10c8901cfd297ce8147cc38ebfe579f443276c9ddc10f80e5817912f21811c7ae526d4e7075ce98bd4177efcf8d111b6050d6f1ef947ecf53373e75d7ec2c5bed3e402fc684bd813baa9ed633e55d41ce75bb01d81fa4fae30ea0dfac38e1032a731eacb227ffd9d7b9391208bb2f859bcfc2cf5ef4571240e47f2e51749964559195cc8ff25ca2391f9069ffe0e197fb636a62ef75c3c29e1ee84d170d8132f3a82111161d4bbbc502e86d2a023ccb11f05c248e2226128f4a4cbcb8ef0e043612489a2d811f4e675fdfbefb10d45612476840564938a1d61d9425dc54141495f54789380800aa3cb8e7095fa214365e90261240d948bde60d8632bcd69d1c328e9f7941f1de1e61dd08ae81f1d417b0f74d01bca978af483e39e451975a130fa8fd8113be26f5cc8ecc8fca1b44d2de8d3fc4d93a469413c4fd434a99826dd52d842996d2945769c6e6927510ae813432ad20c8d9dfd034cafdc4b300403f4d104d9ab76f8a323403bb58591e0ee0932f4c36e238fa9a1dfa1ef4bf5c9320fe1f5159919da5564e46068e87719d0d368764590a15d21271ca7d6bd18591aa09a7f850c4d9540b857a6328ca1ee491b7ff0e8c8e20ec8680fc2d5f05a5eec36bdf9ce0907186a4ae0c837295c2f62a7f99e58e661ff7d7db8dc966b189a8a2c5d79bc5eaf44db3c2063a2e64ecf8a2d7df564e8dfd032aae65363275a60105ade467e4056a8e486ae068e3c7f72e44166ad17e8ba9ef30a6d96ea0cc84ae6842bd1f836be5b2e19ec0db2e45566ada71ed495dc982cc8667d87e07a8e41ae3ec1c954daf41612085781a12f3ca87f43b6397882facab3347567f9eaa3234ba9650e4490f7677cad49454b40354418fede464e3d4b7e18defaea4541f75564c8030c73c0fa82ebd0ca661accac35409bde146fd60bb6be67e80c46dd6fcc79b2312136268ccf085932962db38f36fac073cc15bad654649bd2dee94dc58da6ae1e823dda9883c0d00bfe6e3455b4d69e38cdf7c894efb2cd7a1ab1f536a684599fa50d1eaea3b908429c5979c0f4a18261f28dae3535b8c67719e82d72db1c44b325ffc603212053d9c3cee4ea39cc4a8caf6bbd59f5661cd7076a4c56b9a3ed91adaf28d082364f9051c00ccb27adfb35183b6b7507a2bbe8d6e73ad78ce97cfd61f9acf44ae16b4fc442061a78647a3c7d22436332c78ebec9803e7eb4e57164ad68290b080d8d32ba42680e1eaf23aed306e3df4c839c868da6c65c2e933961bcb6f57166696ae8f40cb469e49c15b0a0a07fa226d64beb2e998e821a3f479efe61997371fa44180ebb4d48c854e23610431d337de0eb30db80a6849d68113b2168dbc36e13c678d35b3c199a87dc5c55365a904df380f3a49afff691206efbda159ae6738643f4011d2ce7bea386aeeca18e774e38a6ce244050c6a2ada981b59e3f82101763beea39e11db2428c1d7df174a2775c27dfd1bbd03657d49adcd47a55e1ff5cafb8dc95690ea8a15bb1651e02f0245d6c97856e19bae2dbe1ea117e6de8aefccead5f3c1b3fa156eb0eebb9f27d69d722da2ef7b3da7e7b5c3ff40acf160f1f6cfd016d26edf13932746967e998cd9f1bba225beb696e9b9cc72283af7855f83b1583683130742534f47166e8630ac295674d825a36562def3bc623a7b1c70a6fc0f9b9e2bac4e1e2f532a8eca367af176451ae39ab693de1994687c684ce2a1f5ae178ebd77caaed8ddb9a7f55f3689acf990e33be1471252ce71c7fd0dee4e91f6fda5b8d23b32388da7ace741cc82b6af935eddc26ebd8265b92135632537cdb47d18ac5251d8bb6c9e28cb5033e8f259263ae32a833fd6cc50f5dc250f776d6e406cd7436d75cb44d2503398f09b51dce96a50fe030372ffa7f26232bec93296fab8f4cceb08c750c67a335ffb5a6eec064113bf2c073f4c3a08e07faea69d39bc6c558bff4fde3dceaadcad804a8314e83eb709c5b398a0adf5beb078b73cc97c4dcd64a1866872d1df24034f5dc2586b365690fdc2f7845fc5e02a6831e08e1d3f7550a67f7ccdf5134951bdc995ffb80fdf1396efde259f56f97dc9f977a081eb9cff956c49d57ecce28e35eeac88b98e92df0d51d0c5739087160ad6f10d43d5cc60c04f5551f16f19ce998d8e6f9c77c578b2fadf54bdf14739f9b5f4556b4ca363d2ea368f6017ebc4c3793ebabfe46657c7b9feeb66e7c886e2e8f9aee709c83703c68fbe2166ea4d09dd2976890db77111740721cef39fcb07cbee54b42c707750ce57c943ee647ac70800c1d737b02b2b703d18214f6c16d1cf1b9f2631f6285fd61e3b7b8ed9ef88f867fefc5ebd25ff358dd923f623c2ae3f853b56ed377f7cbbea22de36b4d153766ca7128e45fedc516c45aaaa2bb56994e30df7aec5ff46fccb7849bf58a424d9537e641b296c7be849f09966ffb1238c1fbf7609a751a9fe344413b66b1bd0929d6dba362ce7d83df572983e681ceee8fec8aef716ffde2d9f2339ce65bbf78b6fa03cbb43c681e44665fd57b638fccc6cbbda5061e99bc98fccb58c36ca488172731ae1c7f513fd91c6fc7b9961cd93e4fbe8a0a5fc37c2468e9e9095c5eee55fc79293783eb0f8b8f336dba823aa6f67a11339f509c9dd859adf89eeddf8d894a3726ce8c6f71799eba43301c53c8e2a3e6ed418853f63d0c574fb3a551ee752dcf99ac707b8f69f87c8fe937fe6afade1e1339f2a63ee35472767a2ad7eb8d79a0eeb2d8bf18fa0203799edb6bb5d279c6b75a6e153e86dff665d342dffdb7f69f53ee87df8169d6f16bbf98bd10230b1dabceba45acacbffd6e0e2447dfc7a5afac6cec68dfc5e3f557529e99f6753fe7cd3da978d4ecd36a9b202dfb6ded63efcab351e15be58db96ff66951c13333078fcca74c2511cdf2635fd88abf642a73bff0a68f2df4e6f45c6479ced7da764abadb67a2cb9d31be431b59c9a03e8e9de886bf83dec2633eb39c9bef9d5b6720ce6bc0746fdcf6a16ae573868606abf7a88eadfa4072cc29068f044de5f2ddaf731431f3c755ec71a255ba095779e5136c73205a26e4df96b464d0947c6b6d94745ca14da8ec1c7de5397cfee9ce91f751f53d1f6bf8e03bb2422d739c35b835e797a95ced796bdc8a7dd93d1b2b62f2b43aafe8e3aceccfdc25a871e1f4e94cde57be13ada8a319b57fb5cd7d49a7e5182187f3d7f59947e5feebd63fdec754749ceef58e7c5e75168fc419ff552c71a3b4496935792b9eef7a33c18c890d3f9a573e81adf2ca43457c3bad3cf82cc9f7d2c5687039927a5f7afd7e7f200d95cbbf9456ee8bfdbf2fadcc31ff99ac724f56944195ffed0f6445e929172f64952f15595144a55781d6343fcf2abf05faab59e5ffdfc58094b53e41377623e846201f7d7ab526e95c0a782e053c97029e4b01cfa580e752c07329e0b914f05c0a782e053c97029e4b01cfa580e752c07f643dd2492aa629437274e57163ee91a363dff82a1669e3c9fc7163f6094f55df9361997ea28e3cf71c4df5e01a11473e04d79a9a58eb2036be8a4daad4dc20d05b04d7d15cdcaca72290f84fa4c4965783eba2ac29e63f7b98fdfa1b43534510ad70f3b3d48a02f9a1992bc44ff755f981aef8364e136b8de16ca98ab6beca9a32952bc4f03226874b43bbcb00c3d7f7769b3076d8faeb65d0a468cdc393b554778e7ed7a428f9cf3b0ab5f43dd9ac57fdebb2af2a7da87eaebf330f94e3e107d4d02dcf99ccf1ac4cd339bae2599ab7b76510df3e5629c4aacc6b2cb99ac7d6a4869e627719a0ef77f54f1fb1e55f79cfe89da88925bd4defeb7c2bd395df3c0f8863ea14785143038fd3fc2aa97098fa1b04f43432f356ff4a44dfef45349571e4844a6e2d8b9fae1cd4947419ba42ade6a71bccf860e838342673d1d2064f501fe770325fdbeb0599f1b5f6689adf44afcb8d2253e66b0f8d16ddc6443c955b6cd5e9f2425739fec5b72fe917e3397273b5d0837b82a6da803af23838e1af72ca4b86d7edbe94634157c8fa1ccd637640cbbeaae4a02a37511d7dcf719a2df7c8ed89f1f555acb079b4f0106ffc6a6dfe2d76270b73b336c854963cdbec47b365f0ab695ab61b06240cdd28a55d48f14753b6c7a055c656e9bd5f08dcbb977ba3be38122fbe5c0c7bfdfe501a2a7f25637b295e0cffbe8c2dc7fca7ea80fb970c9d22b7aaf4861717973db1ff721d701bb4a6f9e53ae0d7407fb90eb82de89f48df9e7e572a56abe7ad9ae1ffedebdb159acf93b52f24a07e3297f44a86e67946a37d5eaaf6e9e7f4ea39bdfa4a7af527f3a9cff5f89c2ffdbf992ffd9fca75fe4456b1ca019e9359ffcc64d6bf2385754e4e9d9353e7e4d4bf3039f5c661f579a2cad2ae1443c729d0951c6a575173576dbf73cc71ecf8036a9bd3aa66326287eeea5b7e203fba0715a0efcba246ed3e18cf1745dd7c71d81f2fac876f0fe57d8cf28e9366f855ddde6bb5ad53b9c20d0ccbbb56b3a37b52651ddb6a8cef16cb41536f5ecdebdff8752d603050ef1f1ea81689171a8aab045cb0e2f5865e629b8360b664fc686abf4afae2928edd75aedede3d48635e1f38593d59e61d82fa25724cdc37749cb1f7a226b97d57b0bc5f52de53717a2bd1d0d3cce9415ec36cafef22901774bfc83b5e3bc8f987ecf50dbfcf6769aae8e46a6aaf11afd97372f5a255c746ab24db755933ba5dbebc36943d0f6a6accfe81b2c695f392af9f2a951c4c398d9d70016b5e628e4f5dd76e68aa33cd6f2243a3c5bd4a9e742a64c3e959ee67db7b3ad3c22a796765c6571159e3c500e80fe4a8de2e0f5af7e114dff6994c160fee5ac5330dd6f78466edf9189d9a57e84e5937f9525dead19abd8237d33c40b3bca5b74b7e4fa8e0c3b2756768c2bf1b96cf4a6f5fafb12eef581dd33dceadfc6378f2e4f137aeb3fc9e41a903ad3b07c5bd92d9134166afa9af2deeb51254d696ff627dad045fa76581817c43cabb8fc7b6ff1a0f5fb8df58dc0b29127ab56f29ef5d323e4c7b55bd3238d60b5e3b7aa5182d9d6073b8bdf7eb328bac90fb6f28e43bffaf7ee752be73aee95cca774e4d9d4bf9cea57ce752be7329dfbf3afb792ee53b97f29db3a5e76ce93f2e5bfae3bf010000ffff010000ffff66f04a38c95f0000`)))