~evanj/cms

bd50253592276552e081cdabd63aac978dfef957 — Evan M Jones 1 year, 5 months ago d76fc72
Fix(cache): When ref'd items are updates, break cache of referers.
M TODO => TODO +5 -2
@@ 1,8 1,11 @@
Handle update of ReferenceList type.
Break cache when a Reference or ReferenceList item has been deleted.
X Handle update of ReferenceList type.
X Break cache when a Reference or ReferenceList item has been deleted.
Cache listicles.
Break cache on content type update.
Allow updating of space.
Cache: When an item is updated, any item that references it is not.
X What happens when you delete a content's only reference list content? Then try to copy space?
X BUG: Removing field from contenttype seems to be broken.
X BUG: create content type, create content, add string field to content type, copy space broken.
Fullscreen takeover for html/markdown editors.
Sidebar nav for desktop

M go.mod => go.mod +1 -0
@@ 10,6 10,7 @@ require (
	github.com/go-playground/assert/v2 v2.0.1
	github.com/go-sql-driver/mysql v1.5.0
	github.com/golang/mock v1.4.3
	github.com/kr/pretty v0.2.0 // indirect
	golang.org/x/crypto v0.0.0-20200320181102-891825fb96df // indirect
	golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
	golang.org/x/text v0.3.2 // indirect

M internal/c/space/space.go => internal/c/space/space.go +3 -3
@@ 47,7 47,7 @@ func New(log *log.Logger, db dber) *Space {
func (s *Space) serve(w http.ResponseWriter, r *http.Request, spaceID string) {
	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to create space")
		s.Error(w, r, http.StatusBadRequest, "must be logged in to view space")
		return
	}



@@ 114,7 114,7 @@ func (s *Space) copy(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to create space")
		s.Error(w, r, http.StatusBadRequest, "must be logged in to copy space")
		return
	}



@@ 141,7 141,7 @@ func (s *Space) delete(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to create space")
		s.Error(w, r, http.StatusBadRequest, "must be logged in to delete space")
		return
	}


M internal/s/cache/content.go => internal/s/cache/content.go +21 -1
@@ 1,12 1,14 @@
package cache

import (
	"errors"
	"fmt"

	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"github.com/bradfitz/gomemcache/memcache"
)

func (c *Cache) content(breakCache bool, key string, getter func() (content.Content, error)) (content.Content, error) {


@@ 39,11 41,29 @@ func (c *Cache) ContentGet(space space.Space, ct contenttype.ContentType, conten
}

func (c *Cache) ContentUpdate(space space.Space, ct contenttype.ContentType, item content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error) {
	return c.content(
	content, err := c.content(
		true,
		fmt.Sprintf("%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), item.ID()),
		func() (content.Content, error) { return c.db.ContentUpdate(space, ct, item, newParams, updateParams) },
	)
	if err != nil {
		return nil, err
	}

	list, err := c.db.ContentRefererList(space, ct, item)
	if err != nil {
		return nil, err
	}

	// Remove content from cache that referenced this.
	for _, ref := range list {
		err := c.mc.Delete(fmt.Sprintf("%s::%s::%s::%s", c.baseKey, space.ID(), ref.ContentTypeID, ref.ContentID))
		if !errors.Is(err, memcache.ErrCacheMiss) { // Don't care about cache miss.
			return nil, err
		}
	}

	return content, nil
}

func (c *Cache) ContentDelete(space space.Space, ct contenttype.ContentType, item content.Content) error {

M internal/s/db/content.go => internal/s/db/content.go +56 -0
@@ 572,6 572,62 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
	return c, t.Commit()
}

type ContentRefSet struct {
	ContentTypeID, ContentID string
}

// ContentRefererList will retreive all content IDs that references a given piece of content.
func (db *DB) ContentRefererList(s space.Space, ct contenttype.ContentType, c content.Content) (ret []ContentRefSet, err error) {
	refQ := `
		SELECT cms_contenttype_to_valuetype.CONTENTTYPE_ID, cms_value.CONTENT_ID FROM cms_value_reference 
		JOIN cms_value ON VALUE_ID = cms_value_reference.ID
		JOIN cms_contenttype_to_valuetype ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		WHERE cms_valuetype.VALUE = ?
		AND cms_value_reference.VALUE = ?
	`

	refListQ := `
		SELECT cms_contenttype_to_valuetype.CONTENTTYPE_ID, cms_value.CONTENT_ID FROM cms_value_reference_list_values
		JOIN cms_value_reference_list ON cms_value_reference_list.ID = cms_value_reference_list_values.VALUE_ID
		JOIN cms_value ON cms_value.VALUE_ID = cms_value_reference_list.ID
		JOIN cms_contenttype_to_valuetype ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		WHERE cms_valuetype.VALUE = ?
		AND cms_value_reference_list_values.CONTENT_ID = ?
	`

	rows, err := db.Query(refQ, valuetype.Reference, c.ID())
	if err != nil {
		return ret, err
	}
	defer rows.Close()

	for rows.Next() {
		var ref ContentRefSet
		if err := rows.Scan(&ref.ContentTypeID, &ref.ContentID); err != nil {
			return ret, err
		}
		ret = append(ret, ref)
	}

	rows, err = db.Query(refListQ, valuetype.ReferenceList, c.ID())
	if err != nil {
		return ret, err
	}
	defer rows.Close()

	for rows.Next() {
		var ref ContentRefSet
		if err := rows.Scan(&ref.ContentTypeID, &ref.ContentID); err != nil {
			return ret, err
		}
		ret = append(ret, ref)
	}

	return ret, nil
}

func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, newParams []ContentNewParam, updateParams []ContentUpdateParam) (content.Content, error) {
	depth := defaultDepth


M internal/s/db/db.go => internal/s/db/db.go +2 -2
@@ 181,8 181,8 @@ func (db *DB) createTables() []error {
		CREATE TABLE cms_value (
			ID INTEGER PRIMARY KEY AUTO_INCREMENT,
			CONTENT_ID INTEGER NOT NULL,
			CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL, -- Should be a foreign key but impossible to make it for two+ tables.
			VALUE_ID INTEGER NOT NULL, 
			CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL, 
			VALUE_ID INTEGER NOT NULL, -- Should be a foreign key but impossible to make it for two+ tables.
			FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID) ON DELETE CASCADE,
			FOREIGN KEY(CONTENTTYPE_TO_VALUETYPE_ID) REFERENCES cms_contenttype_to_valuetype(ID) ON DELETE CASCADE
		);

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +1 -1
@@ 38,7 38,7 @@ func init() {

	tmpls["js/content.js"] = tostring("Ly8gU2V0dXAgaW5wdXRzIGZvciBjb250ZW50IGNyZWF0ZS91cGRhdGUuCihmdW5jdGlvbigpIHsgCgogIC8vIFNhdmUgYnV0dG9uIAoKICB2YXIgc2F2ZUJ0biA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJ2lucHV0W3ZhbHVlPVNhdmVdJykKICBpZiAoc2F2ZUJ0bikgewogICAgc2F2ZUJ0bi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGZ1bmN0aW9uIGNvbnRlbnRVcGRhdGUoZSkgeyAKICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpCiAgICAgIGUuc3RvcFByb3BhZ2F0aW9uKCkKICAgICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvcignZm9ybVthY3Rpb249Ii9jb250ZW50L3VwZGF0ZSJdJykuc3VibWl0KCkKICAgIH0pCiAgfQoKICAvLyBIVE1MCiAgdGlueW1jZS5pbml0KHsgCiAgICBzZWxlY3RvcjogJ3RleHRhcmVhLmlucHV0LWh0bWwnLAogICAgcGx1Z2luczogImNvZGUiLAogICAgZm9yY2VkX3Jvb3RfYmxvY2sgOiAiIiwgLyogTm8gd3JhcHBpbmcgcGFyYWdyYXBoIHRhZy4gKi8KICAgIC8vIHN0YXR1c2JhcjogZmFsc2UsCiAgICBzZXR1cDogZnVuY3Rpb24oaXRlbSkgeyAKICAgICAgaXRlbS5vbignY2hhbmdlJywgZnVuY3Rpb24oKSB7IAogICAgICAgIGl0ZW0udGFyZ2V0RWxtLnZhbHVlID0gaXRlbS5nZXRDb250ZW50KCkKICAgICAgfSkKICAgIH0KICB9KQoKICAvLyBNQVJLRE9XTgogIHRpbnltY2UuaW5pdCh7CiAgICBzZWxlY3RvcjogInRleHRhcmVhLmlucHV0LW1hcmtkb3duIiwKICAgIHBsdWdpbjogJ3RleHRwYXR0ZXJuJywKICAgIGV4dGVybmFsX3BsdWdpbnM6IHsgCiAgICAgIHRleHRwYXR0ZXJuOiAnLy91bnBrZy5jb20vdGlueW1jZUA1LjIuMC9wbHVnaW5zL3RleHRwYXR0ZXJuL3BsdWdpbi5taW4uanMnCiAgICB9LAogICAgbWVudWJhcjogZmFsc2UsCiAgICB0b29sYmFyOiAndW5kbyByZWRvJywKICAgIC8vIHN0YXR1c2JhcjogZmFsc2UsCiAgICB0ZXh0cGF0dGVybl9wYXR0ZXJuczogWwogICAgICB7c3RhcnQ6ICcqJywgZW5kOiAnKicsIGZvcm1hdDogJ2l0YWxpYyd9LAogICAgICB7c3RhcnQ6ICcqKicsIGVuZDogJyoqJywgZm9ybWF0OiAnYm9sZCd9LAogICAgICB7c3RhcnQ6ICdfJywgZW5kOiAnXycsIGZvcm1hdDogJ2JvbGQnfSwKICAgICAge3N0YXJ0OiAnIycsIGZvcm1hdDogJ2gxJ30sCiAgICAgIHtzdGFydDogJyMjJywgZm9ybWF0OiAnaDInfSwKICAgICAge3N0YXJ0OiAnIyMjJywgZm9ybWF0OiAnaDMnfSwKICAgICAge3N0YXJ0OiAnIyMjIycsIGZvcm1hdDogJ2g0J30sCiAgICAgIHtzdGFydDogJyMjIyMjJywgZm9ybWF0OiAnaDUnfSwKICAgICAge3N0YXJ0OiAnIyMjIyMjJywgZm9ybWF0OiAnaDYnfSwKICAgICAge3N0YXJ0OiAnMS4gJywgY21kOiAnSW5zZXJ0T3JkZXJlZExpc3QnfSwKICAgICAge3N0YXJ0OiAnKiAnLCBjbWQ6ICdJbnNlcnRVbm9yZGVyZWRMaXN0J30sCiAgICAgIHtzdGFydDogJy0gJywgY21kOiAnSW5zZXJ0VW5vcmRlcmVkTGlzdCd9CiAgICBdLAogICAgc2V0dXA6IGZ1bmN0aW9uKGl0ZW0pIHsgCiAgICAgIGl0ZW0ub24oJ2NoYW5nZScsIGZ1bmN0aW9uKCkgeyAKICAgICAgICBpdGVtLnRhcmdldEVsbS52YWx1ZSA9IGl0ZW0uZ2V0Q29udGVudCgpCiAgICAgIH0pCiAgICB9CiAgfSk7CgogIC8vIFJFRkVSRU5DRQogIHZhciByZWZzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgiZm9ybSBkaWFsb2ciKQogIHZhciBtZW51cyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoImZvcm0gZGlhbG9nIG1lbnUiKQogIHZhciByZWZidG5zID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgiLmlucHV0LXJlZiIpCiAgdmFyIHRvYnRucyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoIi5vdXRwdXQtcmVmIikKICBmb3IgKGkgPSAwOyBpIDwgcmVmcy5sZW5ndGg7IGkrKykgeyAKICAgIChmdW5jdGlvbihidG4sIG1lbnUsIGRpYWxvZywgb3V0cHV0KSB7IAogICAgICB2YXIgaXNMaXN0ID0gb3V0cHV0LmdldEF0dHJpYnV0ZSgibmFtZSIpLmluZGV4T2YoIlJlZmVyZW5jZUxpc3QiKSAhPSAtMQogICAgICB2YXIgY2xlYXJCdG4gPSBkaWFsb2cucXVlcnlTZWxlY3RvcigiLmxlZnQiKQogICAgICB2YXIgZG9uZUJ0biA9IGRpYWxvZy5xdWVyeVNlbGVjdG9yKCIucmlnaHQiKQoKICAgICAgdmFyIGNob3NlbkNvbnRlbnRUeXBlSUQgLy8gdXNlZCBieSBib3RoCiAgICAgIHZhciBjaG9zZW5Db250ZW50SURzID0gW10gLy8gb25seSB1c2VkIGJlIHJlZmxpc3QKICAgICAgdmFyIGNob3NlbkNvbnRlbnROYW1lcyA9IFtdIC8vIG9ubHkgdXNlZCBiZSByZWZsaXN0CgogICAgICAvLyBPUEVOCiAgICAgIGJ0bi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGZ1bmN0aW9uKGUpIHsgCiAgICAgICAgZS5zdG9wUHJvcGFnYXRpb24oKQogICAgICAgIGUucHJldmVudERlZmF1bHQoKQogICAgICAgIGRpYWxvZy5zaG93TW9kYWwoKQogICAgICB9KQoKICAgICAgLy8gQ0xPU0UKICAgICAgZGlhbG9nLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZnVuY3Rpb24oZSkgeyAKICAgICAgICBlLnN0b3BQcm9wYWdhdGlvbigpCiAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpCiAgICAgICAgaWYgKGlzTGlzdCkgeyAKICAgICAgICAgIC8vIERvbid0IGxldCByZWZsaXN0IGlucHV0IGNsb3NlIGJ5IG9mZiBjbGljaywgdXNlciBtdXN0IGNob29zZSB0bwogICAgICAgICAgLy8gY2xlYXIgaW5wdXQgdG8gY2xvc2UsIG9yIGJlIGRvbmUgdG8gY2xvc2UuCiAgICAgICAgICByZXR1cm4gCiAgICAgICAgfQogICAgICAgIGRpYWxvZy5jbG9zZSgpCiAgICAgIH0pCgogICAgICAvLyBTVE9QCiAgICAgIG1lbnUuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCBmdW5jdGlvbihlKSB7IAogICAgICAgIGUuc3RvcFByb3BhZ2F0aW9uKCkKICAgICAgICBlLnByZXZlbnREZWZhdWx0KCkKICAgICAgfSkKCiAgICAgIGlmIChpc0xpc3QpIHsKICAgICAgICAvLyBDTEVBUgogICAgICAgIGNsZWFyQnRuLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgY2xlYXJCdG5IYW5kbGUpCiAgICAgICAgZnVuY3Rpb24gY2xlYXJCdG5IYW5kbGUoZSkgeyAKICAgICAgICAgIGUuc3RvcFByb3BhZ2F0aW9uKCkKICAgICAgICAgIGUucHJldmVudERlZmF1bHQoKQogICAgICAgICAgb3V0cHV0LnZhbHVlID0gJycKICAgICAgICAgIGJ0bi52YWx1ZSA9ICdPcGVuJwogICAgICAgICAgY2hvc2VuQ29udGVudElEcyA9IFtdCiAgICAgICAgICBjaG9zZW5Db250ZW50TmFtZXMgPSBbXQogICAgICAgICAgZGlhbG9nLmNsb3NlKCkKICAgICAgICB9CgogICAgICAgIC8vIERPTkUKICAgICAgICBkb25lQnRuLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZnVuY3Rpb24oZSkgeyAKICAgICAgICAgIGlmIChjaG9zZW5Db250ZW50SURzLmxlbmd0aCA8IDEpIHsKICAgICAgICAgICAgcmV0dXJuIGNsZWFyQnRuSGFuZGxlKGUpCiAgICAgICAgICB9CiAgICAgICAgICBlLnN0b3BQcm9wYWdhdGlvbigpCiAgICAgICAgICBlLnByZXZlbnREZWZhdWx0KCkKICAgICAgICAgIG91dHB1dC52YWx1ZSA9IGNob3NlbkNvbnRlbnRJRHMuam9pbignLScpCiAgICAgICAgICBidG4udmFsdWUgPSBjaG9zZW5Db250ZW50TmFtZXMuam9pbignLCAnKQogICAgICAgICAgY2hvc2VuQ29udGVudElEcyA9IFtdCiAgICAgICAgICBjaG9zZW5Db250ZW50TmFtZXMgPSBbXQogICAgICAgICAgZGlhbG9nLmNsb3NlKCkKICAgICAgICB9KQogICAgICB9CgogICAgICAvLyBJTlBVVFMgRVZFTlRTIEFORCBSRVNVTFRTCiAgICAgIHZhciBpbnB1dHMgPSBkaWFsb2cucXVlcnlTZWxlY3RvckFsbCgnaW5wdXQnKQogICAgICB2YXIgY29udGVudHR5cGUgPSBpbnB1dHNbMF0KICAgICAgdmFyIGNvbnRlbnQgPSBpbnB1dHNbMV0KCiAgICAgIHZhciBvcHRzID0gewogICAgICAgIGF1dG9zZWxlY3Q6IHRydWUsCiAgICAgICAgYXV0b3NlbGVjdE9uQmx1cjogdHJ1ZSwgCiAgICAgICAgdGFiQXV0b2NvbXBsZXRlOiB0cnVlLAogICAgICAgIC8vIGNsZWFyT25TZWxlY3RlZDogdHJ1ZSwKICAgICAgICBoaW50OiBmYWxzZQogICAgICB9CgogICAgICBmdW5jdGlvbiBnZXRvcHRzKHVybCwgdHJhbnNmb3JtLCBkaXNwbGF5S2V5KSB7IAogICAgICAgIHZhciBjb250ZW50dHlwZUFib3J0ID0gZnVuY3Rpb24oKSB7fQogICAgICAgIHJldHVybiB7CiAgICAgICAgICBkaXNwbGF5S2V5OiBkaXNwbGF5S2V5LAogICAgICAgICAgc291cmNlOiBmdW5jdGlvbihxdWVyeSwgY2IpIHsgCiAgICAgICAgICAgIGNiKFtdKQogICAgICAgICAgICBjb250ZW50dHlwZUFib3J0KCkKICAgICAgICAgICAgdmFyIHJlcSA9IG5ldyBYTUxIdHRwUmVxdWVzdCgpCiAgICAgICAgICAgIGNvbnRlbnR0eXBlQWJvcnQgPSBmdW5jdGlvbigpIHsgcmVxLmFib3J0KCkgfSAKICAgICAgICAgICAgcmVxLm9ucmVhZHlzdGF0ZWNoYW5nZSA9IGZ1bmN0aW9uKCkgewogICAgICAgICAgICAgIGlmICh0aGlzLnJlYWR5U3RhdGUgIT0gNCkgewogICAgICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICBpZiAodGhpcy5zdGF0dXMgIT0gMjAwKSB7CiAgICAgICAgICAgICAgICBpZiAodGhpcy5yZXNwb25zZVRleHQgIT0gIiIpIHsKICAgICAgICAgICAgICAgICAgYWxlcnQodGhpcy5yZXNwb25zZVRleHQpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBjYihbXSkKICAgICAgICAgICAgICAgIHJldHVybgogICAgICAgICAgICAgIH0KCiAgICAgICAgICAgICAgdHJ5IHsgCiAgICAgICAgICAgICAgICBjYih0cmFuc2Zvcm0oSlNPTi5wYXJzZSh0aGlzLnJlc3BvbnNlVGV4dCkpKQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgICBjYXRjaChlKSB7IAogICAgICAgICAgICAgICAgdmFyIG1zZyA9IGUudG9TdHJpbmcoKQogICAgICAgICAgICAgICAgY29uc29sZS5sb2coe2UsbXNnfSkKICAgICAgICAgICAgICAgIGlmIChtc2cgIT0gIiIpIHsgLy8gQ2FuY2VsbGVkIHJlcXVlc3RzIGhpdCB0aGlzLgogICAgICAgICAgICAgICAgICBhbGVydChtc2cpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHJlcS5vcGVuKCdHRVQnLCB1cmwoKSArIHF1ZXJ5LCB0cnVlKQogICAgICAgICAgICByZXEuc2VuZCgpCiAgICAgICAgICB9CiAgICAgICAgfQogICAgICB9CgogICAgICB2YXIgY29udGVudHR5cGVPcHRzID0gZ2V0b3B0cygKICAgICAgICBmdW5jdGlvbigpIHsgcmV0dXJuICcvY29udGVudHR5cGUvc2VhcmNoP3NwYWNlPXt7IC5TcGFjZS5JRCB9fSZxdWVyeT0nOyB9LCAKICAgICAgICBmdW5jdGlvbihkYXRhKSB7IHJldHVybiBkYXRhIH0sCiAgICAgICAgJ0NvbnRlbnRUeXBlTmFtZScKICAgICAgKQoKICAgICAgd2luZG93LmF1dG9jb21wbGV0ZShjb250ZW50dHlwZSwgb3B0cywgW2NvbnRlbnR0eXBlT3B0c10pLm9uKCdhdXRvY29tcGxldGU6c2VsZWN0ZWQnLCBvbkNvbnRlbnRUeXBlU2VsZWN0ZWQpCiAgICAgIGZ1bmN0aW9uIG9uQ29udGVudFR5cGVTZWxlY3RlZChlLCBpdGVtLCBkYXRhc2V0LCBjdHgpIHsKICAgICAgICBjaG9zZW5Db250ZW50VHlwZUlEID0gaXRlbS5Db250ZW50VHlwZUlECiAgICAgICAgY29udGVudC5kaXNhYmxlZCA9IGZhbHNlCiAgICAgIH0KCiAgICAgIHZhciBjb250ZW50T3B0cyA9IGdldG9wdHMoCiAgICAgICAgZnVuY3Rpb24oKSB7IHJldHVybiAnL2NvbnRlbnQvc2VhcmNoP3NwYWNlPXt7IC5TcGFjZS5JRCB9fSZjb250ZW50dHlwZT0nICsgY2hvc2VuQ29udGVudFR5cGVJRCArICcmcXVlcnk9JzsgfSwgCiAgICAgICAgZnVuY3Rpb24oZGF0YSkgeyAKICAgICAgICAgIC8vIEJpZyBoYWNrLgogICAgICAgICAgZGF0YSA9IGRhdGEgPyBkYXRhIDogW10KICAgICAgICAgIGZvciAoaSA9IDA7IGkgPCBkYXRhLmxlbmd0aDsgaSsrKSB7IC8vIFRoaXMgcmVzcG9uc2UgaXMgcGFnZWQsIGRvbid0IHdvcnJ5IGFib3V0IE9eMi4gTWF4IG9mIDIwIGl0ZW1zLgogICAgICAgICAgICBmb3IgKGogPSAwOyBqIDwgZGF0YVtpXS5Db250ZW50VmFsdWVzLmxlbmd0aDsgaisrKSB7CiAgICAgICAgICAgICAgaWYgKGRhdGFbaV0uQ29udGVudFZhbHVlc1tqXS5GaWVsZE5hbWUgPT0gIm5hbWUiKSB7IC8vIFdlJ3JlIGd1YXJhbnRlZWQgdG8gaGF2ZSB0aGlzLgogICAgICAgICAgICAgICAgT2JqZWN0LmFzc2lnbihkYXRhW2ldLCBkYXRhW2ldLkNvbnRlbnRWYWx1ZXNbal0pCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICB9CiAgICAgICAgICByZXR1cm4gZGF0YQogICAgICAgIH0sCiAgICAgICAgJ0ZpZWxkVmFsdWUnCiAgICAgICkKCiAgICAgIC8vIFRPRE86IFdlaXJkIGJlaGF2aW9yIGhlcmUsIHdoeSBkbyBJIGhhdmUgdG8gaW5saW5lIHRoaXMgY2xlYXIgb24KICAgICAgLy8gc2VsZWN0ZWQ/IFdoeSBjYW4ndCBpdCBleGlzdHMgaW4gY29udGVudE9wdHM/CiAgICAgIHdpbmRvdy5hdXRvY29tcGxldGUoY29udGVudCwgT2JqZWN0LmFzc2lnbih7fSwgb3B0cywge2NsZWFyT25TZWxlY3RlZDp0cnVlfSksIFtjb250ZW50T3B0c10pLm9uKCdhdXRvY29tcGxldGU6c2VsZWN0ZWQnLCBvbkNvbnRlbnRTZWxlY3RlZCkKICAgICAgZnVuY3Rpb24gb25Db250ZW50U2VsZWN0ZWQoZSwgaXRlbSwgZGF0YXNldCwgY3R4KSB7CiAgICAgICAgaWYgKGlzTGlzdCkgewogICAgICAgICAgY2hvc2VuQ29udGVudElEcy5wdXNoKGl0ZW0uQ29udGVudElEKQogICAgICAgICAgY2hvc2VuQ29udGVudE5hbWVzLnB1c2goaXRlbS5GaWVsZFZhbHVlKQogICAgICAgICAgYnRuLnZhbHVlID0gY2hvc2VuQ29udGVudE5hbWVzLmpvaW4oJywgJykKICAgICAgICB9CiAgICAgICAgZWxzZSB7CiAgICAgICAgICBvdXRwdXQudmFsdWUgPSBpdGVtLkNvbnRlbnRJRAogICAgICAgICAgYnRuLnZhbHVlID0gaXRlbS5GaWVsZFZhbHVlCiAgICAgICAgICBkaWFsb2cuY2xvc2UoKQogICAgICAgIH0KICAgICAgfQoKICAgIH0pKHJlZmJ0bnNbaV0sIG1lbnVzW2ldLCByZWZzW2ldLCB0b2J0bnNbaV0pCiAgfQoKfSkoKTsK")

	tmpls["js/main.js"] = tostring("Ly8gT24gbW9kYWwgb3BlbiBhbHdheXMgZm9jdXMgZmlyc3QgaW5wdXQuCihmdW5jdGlvbigpIHsgCiAgJCgnLm1vZGFsJykub24oJ3Nob3duLmJzLm1vZGFsJywgZnVuY3Rpb24oKSB7IAogICAgdmFyIGlucHV0ID0gJCh0aGlzKS5maW5kKCdpbnB1dCcpLmZpcnN0KCk7CiAgICB2YXIgYnV0dG9uID0gJCh0aGlzKS5maW5kKCdidXR0b24nKS5maXJzdCgpOwogICAgdmFyIGZpcnN0ID0gaW5wdXQubGVuZ3RoID4gMAogICAgICA/IGlucHV0CiAgICAgIDogYnV0dG9uOwogICAgZmlyc3QuZm9jdXMoKTsKICB9KTsKfSkoKTsK")
	tmpls["js/main.js"] = tostring("Ly8gT24gbW9kYWwgb3BlbiBhbHdheXMgZm9jdXMgZmlyc3QgaW5wdXQgb3IgY2xvc2UgYnV0dG9uLgooZnVuY3Rpb24oKSB7IAogICQoJy5tb2RhbCcpLm9uKCdzaG93bi5icy5tb2RhbCcsIGZ1bmN0aW9uKCkgeyAKICAgIHZhciBpbnB1dCA9ICQodGhpcykuZmluZCgnaW5wdXQnKS5maXJzdCgpOwogICAgdmFyIGJ1dHRvbiA9ICQodGhpcykuZmluZCgnYnV0dG9uJykuZmlyc3QoKTsKICAgIHZhciBmaXJzdCA9IGlucHV0Lmxlbmd0aCA+IDAKICAgICAgPyBpbnB1dAogICAgICA6IGJ1dHRvbjsKICAgIGZpcnN0LmZvY3VzKCk7CiAgfSk7Cn0pKCk7Cg==")

	tmpls["js/space.js"] = tostring("Ly8gQWRkIG1vcmUgZmllbGRzIHRvIHNwYWNlIGNyZWF0ZS4KKGZ1bmN0aW9uKCkgeyAKICB2YXIgYWRkRmllbGRCdG4gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnYWRkLWZpZWxkYnRuJykKICB2YXIgaSA9IDEKICBhZGRGaWVsZEJ0bi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGZ1bmN0aW9uKGUpIHsgCiAgICBpKysKICAgIGUucHJldmVudERlZmF1bHQoKQogICAgZS5zdG9wUHJvcGFnYXRpb24oKQogICAgdmFyIGVsID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JykKICAgIGVsLmlubmVySFRNTCA9IGAKICAgICAgPGRpdiBjbGFzcz0nY29udGFpbmVyLWZsdWlkIHB4LTAnPgogICAgICAgIDxpbnB1dCBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHJlcXVpcmVkIHR5cGU9dGV4dCBuYW1lPSJmaWVsZF9uYW1lXyR7aX0iIHZhbHVlPSIiIC8+CiAgICAgICAgPGRpdiBjbGFzcz0nZm9ybS1ncm91cCByb3cnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY29sLTYnPgogICAgICAgICAgICA8c2VsZWN0IGNsYXNzPSJ3LTEwMCBmb3JtLWNvbnRyb2wiIHJlcXVpcmVkIG5hbWU9ImZpZWxkX3R5cGVfJHtpfSI+CiAgICAgICAgICAgICAgPG9wdGlvbiBkaXNhYmxlZCB2YWx1ZT5GaWVsZCBUeXBlPC9vcHRpb24+CiAgICAgICAgICAgICAgPG9wdGlvbiBzZWxlY3RlZCB2YWx1ZT0iU3RyaW5nU21hbGwiPlN0cmluZyBTbWFsbDwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IlN0cmluZ0JpZyI+U3RyaW5nIEJpZzwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IklucHV0SFRNTCI+SFRNTDwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IklucHV0TWFya2Rvd24iPk1hcmtkb3duPC9vcHRpb24+CiAgICAgICAgICAgICAgPG9wdGlvbiB2YWx1ZT0iRmlsZSI+RmlsZTwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IkRhdGUiPkRhdGU8L29wdGlvbj4KICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSJSZWZlcmVuY2UiPlJlZmVyZW5jZTwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IlJlZmVyZW5jZUxpc3QiPlJlZmVyZW5jZUxpc3Q8L29wdGlvbj4KICAgICAgICAgICAgPC9zZWxlY3Q+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIDxkaXYgY2xhc3M9J2NvbC02Jz4KICAgICAgICAgICAgPGJ1dHRvbiBpZD0ncmVtb3ZlLWZpZWxkYnRuXyR7aX0nIGNsYXNzPSd3LTEwMCBidG4gYnRuLXByaW1hcnknIHR5cGU9YnV0dG9uPlJlbW92ZSBGaWVsZDwvYnV0dG9uPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAgYAogICAgYWRkRmllbGRCdG4ucGFyZW50Tm9kZS5pbnNlcnRCZWZvcmUoZWwsIGFkZEZpZWxkQnRuKQogICAgdmFyIHJlbW92ZUZpZWxkQnRuID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoYHJlbW92ZS1maWVsZGJ0bl8ke2l9YCkKICAgIHJlbW92ZUZpZWxkQnRuLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZnVuY3Rpb24oZSkgeyAKICAgICAgaS0tCiAgICAgIGUucHJldmVudERlZmF1bHQoKQogICAgICBlLnN0b3BQcm9wYWdhdGlvbigpCiAgICAgIGVsLnBhcmVudE5vZGUucmVtb3ZlQ2hpbGQoZWwpCiAgICB9KQogIH0pCn0pKCk7CgovLyBGb3IgdXBkYXRlOiByZW1vdmUgb2xkIGZpZWxkcwooZnVuY3Rpb24oKSB7IAogIHZhciBidG5zID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgiLmJ0bi1yZW1vdmUiKTsKICBmb3IgKHZhciBlID0gMDsgZSA8IGJ0bnMubGVuZ3RoOyBlKyspIHsKICAgIChmdW5jdGlvbihidG4pIHsKICAgICAgYnRuLmFkZEV2ZW50TGlzdGVuZXIoImNsaWNrIiwgZnVuY3Rpb24gaGFuZGVsQ2xpY2soKSB7IAogICAgICAgIGJ0biA9IGJ0bi5wYXJlbnRFbGVtZW50LnBhcmVudEVsZW1lbnQKICAgICAgICBidG4ucGFyZW50RWxlbWVudC5wYXJlbnRFbGVtZW50LnJlbW92ZUNoaWxkKGJ0bi5wYXJlbnRFbGVtZW50KQogICAgICB9KTsKICAgIH0pKGJ0bnNbZV0pOwogIH0KfSkoKTsK")