diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..41d3411 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/.data/ +/_output/ diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index abfb9be..e14a2ce --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,7 @@ -.DS_Store -*.devhelper -library/DevHelper/DevHelper/Config.php -library/DevHelper/DevHelper/Generated/ -FileSums.php -js/DevHelper/CodeMirror/mode/css/scss_test.js -js/DevHelper/CodeMirror/mode/css/test.js -js/DevHelper/CodeMirror/mode/javascript/test.js -library/DevHelper/DevHelper/XFCP -library/DevHelper/Config.php -.data -docker-compose.override.yml -js/DevHelper/*.min.js +/.data/ +/DevHelper/autogen.json +/_build/ +/_data/ +/_files/dev/ +/_releases/ +/vendor/ \ No newline at end of file diff --git a/.htaccess b/.htaccess deleted file mode 100644 index f9b3625..0000000 --- a/.htaccess +++ /dev/null @@ -1,2 +0,0 @@ -RewriteEngine On -RewriteRule ^.*$ router.php [NC,L] \ No newline at end of file diff --git a/Autogen/Admin/Controller/Entity.php b/Autogen/Admin/Controller/Entity.php new file mode 100644 index 0000000..f40d5ce --- /dev/null +++ b/Autogen/Admin/Controller/Entity.php @@ -0,0 +1,905 @@ +filterPage(); + $perPage = $this->getPerPage(); + + list($finder, $filters) = $this->entityListData(); + + $finder->limitByPage($page, $perPage); + $total = $finder->total(); + + $viewParams = [ + 'entities' => $finder->fetch(), + + 'filters' => $filters, + 'page' => $page, + 'perPage' => $perPage, + 'total' => $total + ]; + + return $this->getViewReply('list', $viewParams); + } + + /** + * @return \XF\Mvc\Reply\AbstractReply + * @throws \Exception + */ + public function actionAdd() + { + if (!$this->supportsAdding()) { + return $this->noPermission(); + } + + return $this->entityAddEdit($this->createEntity()); + } + + /** + * @param ParameterBag $params + * @return \XF\Mvc\Reply\AbstractReply + * @throws \XF\Mvc\Reply\Exception + * @throws \XF\PrintableException + */ + public function actionDelete(ParameterBag $params) + { + if (!$this->supportsDeleting()) { + return $this->noPermission(); + } + + $entityId = $this->getEntityIdFromParams($params); + $entity = $this->assertEntityExists($entityId); + + if ($this->isPost()) { + $entity->delete(); + + return $this->redirect($this->buildLink($this->getRoutePrefix())); + } + + $viewParams = [ + 'entity' => $entity, + 'entityLabel' => $this->getEntityLabel($entity) + ]; + + return $this->getViewReply('delete', $viewParams); + } + + /** + * @param ParameterBag $params + * @return \XF\Mvc\Reply\AbstractReply + * @throws \XF\Mvc\Reply\Exception + * @throws \Exception + */ + public function actionEdit(ParameterBag $params) + { + if (!$this->supportsEditing()) { + return $this->noPermission(); + } + + $entityId = $this->getEntityIdFromParams($params); + $entity = $this->assertEntityExists($entityId); + return $this->entityAddEdit($entity); + } + + /** + * @return \XF\Mvc\Reply\AbstractReply + * @throws \Exception + * @throws \XF\Mvc\Reply\Exception + * @throws \XF\PrintableException + */ + public function actionSave() + { + $this->assertPostOnly(); + + $entityId = $this->filter('entity_id', 'str'); + if ($entityId !== '') { + $entity = $this->assertEntityExists($entityId); + } else { + $entity = $this->createEntity(); + } + + $this->entitySaveProcess($entity)->run(); + + return $this->redirect($this->buildLink($this->getRoutePrefix())); + } + + /** + * @param MvcEntity $entity + * @param string $columnName + * @return string|object|null + */ + public function getEntityColumnLabel($entity, $columnName) + { + /** @var mixed $unknownEntity */ + $unknownEntity = $entity; + $callback = [$unknownEntity, 'getEntityColumnLabel']; + if (!is_callable($callback)) { + $shortName = $entity->structure()->shortName; + throw new \InvalidArgumentException("Entity {$shortName} does not implement {$callback[1]}"); + } + + return call_user_func($callback, $columnName); + } + + /** + * @param MvcEntity $entity + * @return string + */ + public function getEntityExplain($entity) + { + return ''; + } + + /** + * @param MvcEntity $entity + * @return string + */ + public function getEntityHint($entity) + { + $structure = $entity->structure(); + if (isset($structure->columns['display_order'])) { + return sprintf('%s: %d', \XF::phrase('display_order'), $entity->get('display_order')); + } + + return ''; + } + + /** + * @param MvcEntity $entity + * @return mixed + */ + public function getEntityLabel($entity) + { + /** @var mixed $unknownEntity */ + $unknownEntity = $entity; + $callback = [$unknownEntity, 'getEntityLabel']; + if (!is_callable($callback)) { + $shortName = $entity->structure()->shortName; + throw new \InvalidArgumentException("Entity {$shortName} does not implement {$callback[1]}"); + } + + return call_user_func($callback); + } + + /** + * @param int $entityId + * @return MvcEntity + * @throws \XF\Mvc\Reply\Exception + */ + protected function assertEntityExists($entityId) + { + return $this->assertRecordExists($this->getShortName(), $entityId); + } + + /** + * @return MvcEntity + */ + protected function createEntity() + { + return $this->em()->create($this->getShortName()); + } + + /** + * @param MvcEntity $entity + * @return \XF\Mvc\Reply\AbstractReply + * @throws \Exception + */ + protected function entityAddEdit($entity) + { + $viewParams = [ + 'entity' => $entity, + 'columns' => [], + ]; + + $structure = $entity->structure(); + $viewParams['columns'] = $this->entityGetMetadataForColumns($entity); + + foreach ($structure->relations as $relationKey => $relation) { + if (!isset($relation['entity']) || + !isset($relation['type']) || + $relation['type'] !== MvcEntity::TO_ONE || + !isset($relation['primary']) || + !isset($relation['conditions'])) { + continue; + } + + $columnName = ''; + $relationConditions = $relation['conditions']; + if (is_string($relationConditions)) { + $columnName = $relationConditions; + } elseif (is_array($relationConditions)) { + if (count($relationConditions) === 1) { + $relationCondition = reset($relationConditions); + if (count($relationCondition) === 3 && + $relationCondition[1] === '=' && + preg_match('/\$(.+)$/', $relationCondition[2], $matches) === 1 + ) { + $columnName = $matches[1]; + } + } + } + if ($columnName === '' || !isset($viewParams['columns'][$columnName])) { + continue; + } + $columnViewParamRef = &$viewParams['columns'][$columnName]; + list ($relationTag, $relationTagOptions) = $this->entityAddEditRelationColumn( + $entity, + $columnViewParamRef['_structureData'], + $relationKey, + $relation + ); + + if ($relationTag !== null) { + $columnViewParamRef['tag'] = $relationTag; + $columnViewParamRef['tagOptions'] = $relationTagOptions; + } + } + + return $this->getViewReply('edit', $viewParams); + } + + /** + * @param MvcEntity $entity + * @param array $column + * @param string $relationKey + * @param array $relation + * @return array + */ + protected function entityAddEditRelationColumn($entity, array $column, $relationKey, array $relation) + { + $tag = null; + $tagOptions = []; + switch ($relation['entity']) { + case 'XF:Forum': + $tag = 'select'; + /** @var \XF\Repository\Node $nodeRepo */ + $nodeRepo = $entity->repository('XF:Node'); + $tagOptions['choices'] = $nodeRepo->getNodeOptionsData(false, ['Forum']); + break; + case 'XF:User': + $tag = 'username'; + /** @var \XF\Entity\User|null $user */ + $user = $entity->getRelation($relationKey); + $tagOptions['username'] = $user !== null ? $user->username : ''; + break; + default: + if (strpos($relation['entity'], $this->getPrefixForClasses()) === 0) { + $choices = []; + + /** @var MvcEntity $relationEntity */ + foreach ($this->finder($relation['entity'])->fetch() as $relationEntity) { + $choices[] = [ + 'value' => $relationEntity->getEntityId(), + 'label' => $this->getEntityLabel($relationEntity) + ]; + } + + $tag = 'select'; + $tagOptions['choices'] = $choices; + } + } + + if ($tag === 'select') { + if (isset($tagOptions['choices']) && + (!isset($column['required']) || $column['required'] === false) + ) { + array_unshift($tagOptions['choices'], [ + 'value' => 0, + 'label' => '', + ]); + } + } + + return [$tag, $tagOptions]; + } + + /** + * @param MvcEntity $entity + * @param string $columnName + * @param array $column + * @return array|null + * @throws \Exception + */ + protected function entityGetMetadataForColumn($entity, $columnName, array $column) + { + $columnTag = null; + $columnTagOptions = []; + $columnFilter = null; + $requiresLabel = true; + + if (!$entity->exists()) { + if (isset($column['default'])) { + $entity->set($columnName, $column['default']); + } + + if ($this->request->exists($columnName)) { + $input = $this->filter(['filters' => [$columnName => 'str']]); + if ($input['filters'][$columnName] !== '') { + $entity->set($columnName, $this->filter($columnName, $input['filters'][$columnName])); + $requiresLabel = false; + } + } + } else { + if (isset($column['writeOnce']) && $column['writeOnce'] === true) { + // do not render row for write once column, new value won't be accepted anyway + return null; + } + } + + $columnLabel = $this->getEntityColumnLabel($entity, $columnName); + if ($requiresLabel && $columnLabel === null) { + return null; + } + + switch ($column['type']) { + case MvcEntity::BOOL: + $columnTag = 'radio'; + $columnTagOptions = [ + 'choices' => [ + ['value' => 1, 'label' => \XF::phrase('yes')], + ['value' => 0, 'label' => \XF::phrase('no')], + ] + ]; + $columnFilter = 'bool'; + break; + case MvcEntity::INT: + $columnTag = 'number-box'; + $columnFilter = 'int'; + break; + case MvcEntity::UINT: + $columnTag = 'number-box'; + $columnTagOptions['min'] = 0; + $columnFilter = 'uint'; + break; + case MvcEntity::STR: + if (isset($column['allowedValues'])) { + $choices = []; + foreach ($column['allowedValues'] as $allowedValue) { + $label = $allowedValue; + if (is_object($columnLabel) && $columnLabel instanceof \XF\Phrase) { + $labelPhraseName = $columnLabel->getName() . '_' . + preg_replace('/[^a-z]+/i', '_', $allowedValue); + $label = \XF::phraseDeferred($labelPhraseName); + } + + $choices[] = [ + 'value' => $allowedValue, + 'label' => $label + ]; + } + + $columnTag = 'select'; + $columnTagOptions = ['choices' => $choices]; + } elseif (isset($column['maxLength']) && $column['maxLength'] <= 255) { + $columnTag = 'text-box'; + } else { + $columnTag = 'text-area'; + } + $columnFilter = 'str'; + break; + } + + if ($columnTag === null || $columnFilter === null) { + if (isset($column['inputFilter']) && isset($column['macroTemplate'])) { + $columnTag = 'custom'; + $columnFilter = $column['inputFilter']; + } + } + + if ($columnTag === null || $columnFilter === null) { + if (\XF::$debugMode) { + if ($columnTag === null) { + throw new \Exception( + "Cannot render column {$columnName}, " . + "consider putting \`macroTemplate\` in getStructure for custom rendering." + ); + } + + if ($columnFilter === null) { + throw new \Exception( + "Cannot detect filter data type for column {$columnName}, " . + "consider putting \`inputFilter\` in getStructure to continue." + ); + } + } + + return null; + } + + return [ + 'filter' => $columnFilter, + 'label' => $columnLabel, + 'tag' => $columnTag, + 'tagOptions' => $columnTagOptions, + ]; + } + + /** + * @param MvcEntity $entity + * @return array + * @throws \Exception + */ + protected function entityGetMetadataForColumns($entity) + { + $columns = []; + $structure = $entity->structure(); + + $getterColumns = []; + foreach ($structure->getters as $getterKey => $getterCacheable) { + if (!$getterCacheable) { + continue; + } + + $columnLabel = $this->getEntityColumnLabel($entity, $getterKey); + if ($columnLabel === null) { + continue; + } + + $value = $entity->get($getterKey); + if (!($value instanceof \XF\Phrase)) { + continue; + } + + $getterColumns[$getterKey] = [ + 'isGetter' => true, + 'isNotValue' => true, + 'isPhrase' => true, + 'type' => MvcEntity::STR + ]; + } + + $structureColumns = array_merge($getterColumns, $structure->columns); + foreach ($structureColumns as $columnName => $column) { + $metadata = $this->entityGetMetadataForColumn($entity, $columnName, $column); + if (!is_array($metadata)) { + continue; + } + + $columns[$columnName] = $metadata; + $columns[$columnName] += [ + '_structureData' => $column, + 'name' => sprintf('values[%s]', $columnName), + 'value' => $entity->get($columnName), + ]; + } + + return $columns; + } + + /** + * @return array + */ + final protected function entityListData() + { + $shortName = $this->getShortName(); + $finder = $this->finder($shortName); + $filters = ['pageNavParams' => []]; + + /** @var mixed $unknownFinder */ + $unknownFinder = $finder; + $entityDoXfFilter = [$unknownFinder, 'entityDoXfFilter']; + if (is_callable($entityDoXfFilter)) { + $filter = $this->filter('_xfFilter', ['text' => 'str', 'prefix' => 'bool']); + if (strlen($filter['text']) > 0) { + call_user_func($entityDoXfFilter, $filter['text'], $filter['prefix']); + $filters['_xfFilter'] = $filter['text']; + } + } + + $entityDoListData = [$unknownFinder, 'entityDoListData']; + if (is_callable($entityDoListData)) { + $filters = call_user_func($entityDoListData, $this, $filters); + } else { + $structure = $this->em()->getEntityStructure($shortName); + if (isset($structure->columns['display_order'])) { + $finder->setDefaultOrder('display_order'); + } + } + + return [$finder, $filters]; + } + + /** + * @param MvcEntity $entity + * @return FormAction + * @throws \Exception + */ + protected function entitySaveProcess($entity) + { + $filters = []; + $columns = $this->entityGetMetadataForColumns($entity); + foreach ($columns as $columnName => $metadata) { + if (isset($metadata['_structureData']['isNotValue']) + && $metadata['_structureData']['isNotValue'] === true + ) { + continue; + } + + $filters[$columnName] = $metadata['filter']; + } + + $form = $this->formAction(); + $input = $this->filter(['values' => $filters]); + $form->basicEntitySave($entity, $input['values']); + + $form->setup(function (FormAction $form) use ($columns, $entity) { + $input = $this->filter([ + 'hidden_columns' => 'array-str', + 'hidden_values' => 'array-str', + 'values' => 'array', + ]); + + foreach ($input['hidden_columns'] as $columnName) { + $entity->set( + $columnName, + isset($input['hidden_values'][$columnName]) ? $input['hidden_values'][$columnName] : '' + ); + } + + foreach ($columns as $columnName => $metadata) { + if (!isset($input['values'][$columnName])) { + continue; + } + + if (isset($metadata['_structureData']['isPhrase']) && + $metadata['_structureData']['isPhrase'] === true + ) { + /** @var mixed $unknownEntity */ + $unknownEntity = $entity; + $callable = [$unknownEntity, 'getMasterPhrase']; + if (is_callable($callable)) { + /** @var \XF\Entity\Phrase $masterPhrase */ + $masterPhrase = call_user_func($callable, $columnName); + $masterPhrase->phrase_text = $input['values'][$columnName]; + $entity->addCascadedSave($masterPhrase); + } + } + } + }); + + $form->setup(function (FormAction $form) use ($entity) { + $input = $this->filter([ + 'username_columns' => 'array-str', + 'username_values' => 'array-str', + ]); + + foreach ($input['username_columns'] as $columnName) { + $userId = 0; + + if (isset($input['username_values'][$columnName])) { + /** @var \XF\Repository\User $userRepo */ + $userRepo = $this->repository('XF:User'); + /** @var \XF\Entity\User|null $user */ + $user = $userRepo->getUserByNameOrEmail($input['username_values'][$columnName]); + if ($user === null) { + $form->logError(\XF::phrase('requested_user_not_found')); + } else { + $userId = $user->user_id; + } + } + + $entity->set($columnName, $userId); + } + }); + + return $form; + } + + /** + * @param ParameterBag $params + * @return int + */ + protected function getEntityIdFromParams(ParameterBag $params) + { + $structure = $this->em()->getEntityStructure($this->getShortName()); + if (is_string($structure->primaryKey)) { + return $params->get($structure->primaryKey); + } + + return 0; + } + + /** + * @return int + */ + protected function getPerPage() + { + return 20; + } + + /** + * @return array + */ + protected function getViewLinks() + { + $routePrefix = $this->getRoutePrefix(); + $links = [ + 'index' => $routePrefix, + 'save' => sprintf('%s/save', $routePrefix) + ]; + + if ($this->supportsAdding()) { + $links['add'] = sprintf('%s/add', $routePrefix); + } + + if ($this->supportsDeleting()) { + $links['delete'] = sprintf('%s/delete', $routePrefix); + } + + if ($this->supportsEditing()) { + $links['edit'] = sprintf('%s/edit', $routePrefix); + } + + if ($this->supportsViewing()) { + $links['view'] = sprintf('%s/view', $routePrefix); + } + + if ($this->supportsXfFilter()) { + $links['quickFilter'] = $routePrefix; + } + + return $links; + } + + /** + * @return array + */ + protected function getViewPhrases() + { + $prefix = $this->getPrefixForPhrases(); + + $phrases = []; + foreach ([ + 'add', + 'edit', + 'entities', + 'entity', + ] as $partial) { + $phrases[$partial] = \XF::phrase(sprintf('%s_%s', $prefix, $partial)); + } + + return $phrases; + } + + /** + * @param string $action + * @param array $viewParams + * @return \XF\Mvc\Reply\View + */ + protected function getViewReply($action, array $viewParams) + { + $viewClass = sprintf('%s\Entity%s', $this->getShortName(), ucwords($action)); + $templateTitle = sprintf('%s_entity_%s', $this->getPrefixForTemplates(), strtolower($action)); + + $viewParams['controller'] = $this; + $viewParams['links'] = $this->getViewLinks(); + $viewParams['phrases'] = $this->getViewPhrases(); + + return $this->view($viewClass, $templateTitle, $viewParams); + } + + /** + * @return bool + */ + protected function supportsAdding() + { + return true; + } + + /** + * @return bool + */ + protected function supportsDeleting() + { + return true; + } + + /** + * @return bool + */ + protected function supportsEditing() + { + return true; + } + + /** + * @return bool + */ + protected function supportsViewing() + { + return false; + } + + /** + * @return bool + */ + protected function supportsXfFilter() + { + /** @var mixed $unknownFinder */ + $unknownFinder = $this->finder($this->getShortName()); + return is_callable([$unknownFinder, 'entityDoXfFilter']); + } + + /** + * @return string + */ + abstract protected function getShortName(); + + /** + * @return string + */ + abstract protected function getPrefixForClasses(); + + /** + * @return string + */ + abstract protected function getPrefixForPhrases(); + + /** + * @return string + */ + abstract protected function getPrefixForTemplates(); + + /** + * @return string + */ + abstract protected function getRoutePrefix(); + + // DevHelper/Autogen begins + + /** + * @param \DevHelper\Util\AutogenContext $context + * @return void + * @throws \XF\PrintableException + * @throws \ReflectionException + */ + public function devHelperAutogen($context) + { + $context->gitignoreDeletes[] = '/DevHelper/Admin/Controller/Entity.php'; + + $entity = $this->createEntity(); + + $implementHints = $this->devHelperGetImplementHints($entity); + if (count($implementHints) > 0) { + $context->writeln(sprintf("%s should implement these:\n", $this->getShortName())); + $context->writeln(implode("\n", $implementHints)); + } + + $structure = $entity->structure(); + \DevHelper\Util\Autogen\AdminRoute::autogen( + $context, + $this->getRoutePrefix(), + $structure->primaryKey, + str_replace('\Admin\Controller\\', ':', get_class($this)) + ); + + $phraseTitlePartials = ['_entities', '_entity']; + if ($this->supportsAdding()) { + $phraseTitlePartials[] = '_add'; + } + if ($this->supportsEditing()) { + $phraseTitlePartials[] = '_edit'; + } + foreach ($phraseTitlePartials as $phraseTitlePartial) { + \DevHelper\Util\Autogen\Phrase::autogen($context, $this->getPrefixForPhrases() . $phraseTitlePartial); + } + + $prefixForTemplates = $this->getPrefixForTemplates(); + $templateTitlePartials = ['list']; + if ($this->supportsDeleting()) { + $templateTitlePartials[] = 'delete'; + } + if ($this->supportsEditing()) { + $templateTitlePartials[] = 'edit'; + } + foreach ($templateTitlePartials as $templateTitlePartial) { + $templateTitleSource = "devhelper_autogen_ace_{$templateTitlePartial}"; + $templateTitleTarget = "{$prefixForTemplates}_entity_{$templateTitlePartial}"; + \DevHelper\Util\Autogen\AdminTemplate::autogen($context, $templateTitleSource, $templateTitleTarget); + } + + $context->writeln( + '' . get_class($this) . ' OK', + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE + ); + } + + /** + * @param MvcEntity $entity + * @return array + * @throws \ReflectionException + */ + protected function devHelperGetImplementHints(MvcEntity $entity) + { + $implementHints = []; + $docComments = []; + $methods = []; + + $entityClass = get_class($entity); + /** @var \ReflectionClass|null $rc */ + $rc = new \ReflectionClass($entityClass); + while (true) { + $parent = $rc->getParentClass(); + if ($parent === false || $parent->getName() === 'XF\Mvc\Entity\Entity' || $parent->isAbstract()) { + break; + } + + $rc = $parent; + $entityClass = $rc->getName(); + } + $rcDocComment = $rc->getDocComment(); + if ($rcDocComment === false) { + $entityClassAsArg = $entityClass; + $entityClassAsArg = str_replace('\\Entity\\', ':', $entityClassAsArg); + $entityClassAsArg = str_replace('\\', '\\\\', $entityClassAsArg); + $docComments[] = "/**\n * Run `xf-dev--entity-class-properties.sh {$entityClassAsArg}`\n */"; + } + + $t = str_repeat(" ", 4); + if ($this->supportsAdding() || $this->supportsEditing()) { + try { + $this->getEntityColumnLabel($entity, __METHOD__); + } catch (\InvalidArgumentException $e) { + $methods[] = "{$t}public function getEntityColumnLabel(\$columnName)\n{$t}{\n" . + "{$t}{$t}switch (\$columnName) {\n" . + "{$t}{$t}{$t}case 'column_1':\n" . + "{$t}{$t}{$t}case 'column_2':\n" . + "{$t}{$t}{$t}{$t}return \\XF::phrase('{$this->getPrefixForPhrases()}_' . \$columnName);\n" . + "{$t}{$t}}\n\n" . + "{$t}{$t}return null;\n" . + "{$t}}\n"; + } catch (\Exception $e) { + // ignore + } + } + try { + $this->getEntityLabel($entity); + } catch (\InvalidArgumentException $e) { + $methods[] = "{$t}public function getEntityLabel()\n{$t}{\n{$t}{$t}return \$this->column_name;\n{$t}}\n"; + } catch (\Exception $e) { + // ignore + } + + if (count($docComments) === 0 && count($methods) === 0) { + return $implementHints; + } + + if (count($docComments) > 0) { + foreach ($docComments as $docComment) { + $implementHints[] = $docComment; + } + } + + $implementHints[] = "class X extends Entity\n{\n"; + + if (count($methods) > 0) { + $implementHints[] = "{$t}...\n"; + foreach ($methods as $method) { + $implementHints[] = $method; + } + $implementHints[] = "{$t}...\n"; + } + + $implementHints[] = '}'; + + return $implementHints; + } + + // DevHelper/Autogen ends +} diff --git a/Cli/Command/AutoCheck.php b/Cli/Command/AutoCheck.php new file mode 100644 index 0000000..5b9c929 --- /dev/null +++ b/Cli/Command/AutoCheck.php @@ -0,0 +1,129 @@ +offsetGet('additional_files'); + $filesJsPath = "js/{$addOn->getAddOnId()}"; + $filesStylesPath = "styles/default/{$addOn->getAddOnId()}"; + + $includedJs = false; + $includedStyles = false; + foreach ((array)$additionalFiles as $additionalFile) { + if ($additionalFile === $filesJsPath) { + $includedJs = true; + continue; + } + + if ($additionalFile === $filesStylesPath) { + $includedStyles = true; + continue; + } + } + + if (!$includedJs) { + $filesJsFullPath = $addOn->getFilesDirectory() . '/' . $filesJsPath; + if (is_dir($filesJsFullPath)) { + $output->writeln("JS directory found, {$filesJsPath} should be in `build.json`"); + $result = 1; + } + } + + if (!$includedStyles) { + $filesStylesFullPath = $addOn->getFilesDirectory() . '/' . $filesStylesPath; + if (is_dir($filesStylesFullPath)) { + $output->writeln("Styles directory found, {$filesStylesPath} should be in `build.json`"); + $result = 1; + } + } + + return $result; + } + + /** + * @param AddOn $addOn + * @param OutputInterface $output + * @return int + */ + public function checkPurchasables(AddOn $addOn, OutputInterface $output) + { + $result = 0; + $app = \XF::app(); + + $purchasables = $app->finder('XF:Purchasable') + ->where('addon_id', $addOn->getAddOnId()) + ->fetch(); + + /** @var \XF\Entity\Purchasable $purchasable */ + foreach ($purchasables as $purchasable) { + /** @var \XF\Finder\Template $templateFinder */ + $templateFinder = $app->finder('XF:Template'); + + $emailTemplateTitle = 'payment_received_receipt_' . $purchasable->purchasable_type_id; + $emailTemplateCount = $templateFinder->fromAddOn($addOn->getAddOnId()) + ->where('type', 'email') + ->where('title', $emailTemplateTitle) + ->total(); + + if ($emailTemplateCount === 0) { + $output->writeln("Template email:{$emailTemplateTitle} does not exist"); + $result = 1; + } + } + + return $result; + } + + /** + * @return void + */ + protected function configure() + { + $this + ->setName('devhelper:autocheck') + ->setDescription('Check common mistakes for the specified add-on') + ->addArgument( + 'id', + InputArgument::REQUIRED, + 'Add-On ID' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $id = $input->getArgument('id'); + $addOn = $this->checkEditableAddOn($id, $error); + if (!$addOn) { + $output->writeln('' . $error . ''); + return 1; + } + + $result = 0; + $result |= $this->checkBuildJson($addOn, $output); + $result |= $this->checkPurchasables($addOn, $output); + + if ($result === 0) { + $output->writeln("autocheck OK"); + } + + return $result; + } +} diff --git a/Cli/Command/AutoGen.php b/Cli/Command/AutoGen.php new file mode 100644 index 0000000..a16e927 --- /dev/null +++ b/Cli/Command/AutoGen.php @@ -0,0 +1,330 @@ +devHelperDirPath = dirname(dirname(__DIR__)); + } + + /** + * @param OutputInterface $output + * @return void + * @throws \XF\PrintableException + */ + public function assertDevHelperLatest(OutputInterface $output) + { + $devHelperAddOn = $this->checkInstalledAddOn('DevHelper'); + if ($devHelperAddOn === null) { + throw new \LogicException('DevHelper add-on must be installed'); + } + $devHelperInstalledVersionId = $devHelperAddOn->getInstalledAddOn()->version_id; + $devHelperJsonVersionId = $devHelperAddOn->getJsonVersion()['version_id']; + if ($devHelperInstalledVersionId !== $devHelperJsonVersionId) { + throw new \LogicException("DevHelper add-on must be upgraded ({$devHelperInstalledVersionId} vs. {$devHelperJsonVersionId})"); + } + + $entity = $devHelperAddOn->getInstalledAddOn(); + if ($entity === null) { + throw new \RuntimeException('Cannot get add-on entity'); + } + if (!$entity->active) { + $entity->active = true; + $entity->save(); + + $output->writeln('Enabled DevHelper automatically'); + } + } + + /** + * @param AddOn $addOn + * @param OutputInterface $output + * @return array + */ + public function autoGenLoad(AddOn $addOn, OutputInterface $output) + { + $autoGenPath = $this->autoGenPath($addOn); + $autoGenLegacyPath = $this->autoGenPath($addOn, true); + if (file_exists($autoGenLegacyPath)) { + if (file_exists($autoGenPath)) { + throw new \LogicException("{$autoGenLegacyPath} must be moved to {$autoGenPath} manually"); + } else { + File::writeFile($autoGenPath, file_get_contents($autoGenLegacyPath), false); + unlink($autoGenLegacyPath); + $output->writeln("{$autoGenLegacyPath} has been moved to {$autoGenPath} automatically"); + } + } + + $autoGen = []; + if (file_exists($autoGenPath)) { + $autoGenContents = file_get_contents($autoGenPath); + if (is_string($autoGenContents)) { + $autoGen = @json_decode($autoGenContents, true); + if (!is_array($autoGen)) { + $autoGen = []; + } + } + } + $lastAutoGenRun = []; + if (isset($autoGen[__CLASS__])) { + $lastAutoGenRun = $autoGen[__CLASS__]; + } + if (count($lastAutoGenRun) > 0 && $lastAutoGenRun['version_id'] > self::VERSION_ID) { + throw new \LogicException("{$autoGenPath} was generated by a newer version"); + } + + return $autoGen; + } + + /** + * @param AddOn $addOn + * @param bool $legacy + * @return string + */ + public function autoGenPath(AddOn $addOn, $legacy = false) + { + if (!$legacy) { + return $addOn->getFilesDirectory() . '/dev/autogen.json'; + } else { + return $addOn->getAddOnDirectory() . '/DevHelper/autogen.json'; + } + } + + /** + * @param array $autoGen + * @param AutogenContext $context + * @return void + * @throws \ReflectionException + */ + public function doAdminControllerEntity(array &$autoGen, AutogenContext $context) + { + $addOnId = $context->getAddOnId(); + $addOnDir = $context->getAddOnDirectory(); + $classNamePrefix = str_replace('/', '\\', $addOnId); + $baseNamespace = "{$classNamePrefix}\\DevHelper\\Admin\\Controller"; + $baseClass = "{$baseNamespace}\\Entity"; + $basePathPartial = 'Admin/Controller/Entity.php'; + $basePathSource = "{$this->devHelperDirPath}/Autogen/{$basePathPartial}"; + $basePathTarget = "{$addOnDir}/DevHelper/{$basePathPartial}"; + $controllerDirPath = "{$addOnDir}/Admin/Controller"; + $controllerNamespace = "{$classNamePrefix}\\Admin\\Controller"; + + if (!is_dir($controllerDirPath)) { + return; + } + + $baseClassRefFound = false; + $controllerClasses = []; + foreach (new \DirectoryIterator($controllerDirPath) as $entry) { + // TODO: process sub-directories + + $entryExtension = $entry->getExtension(); + if ($entryExtension !== 'php') { + continue; + } + + $entryFileName = $entry->getFilename(); + $entryClassName = substr($entryFileName, 0, -strlen($entryExtension) - 1); + $controllerClasses[] = "{$controllerNamespace}\\{$entryClassName}"; + + $entryContents = file_get_contents($entry->getPath()); + if (!is_string($entryContents)) { + continue; + } + + if (strpos($entryContents, $baseClass) !== false) { + $baseClassRefFound = true; + } + } + + if ($baseClassRefFound) { + return; + } + + $baseContents = file_get_contents($basePathSource); + if (!is_string($baseContents)) { + return; + } + + $baseContents = preg_replace('/namespace .+;/', "namespace {$baseNamespace};", $baseContents); + $this->extractVersion($basePathPartial, $baseContents, $autoGen); + if (!File::writeFile($basePathTarget, $baseContents, false)) { + throw new \LogicException("Cannot copy {$basePathSource} -> {$basePathTarget}"); + } + + foreach ($controllerClasses as $controllerClass) { + $reflectionClass = new \ReflectionClass($controllerClass); + if ($reflectionClass->isAbstract()) { + continue; + } + + $controller = $context->newController($controllerClass); + $context->executeDevHelperAutogen($controller); + } + + $pattern = '#\s+' . preg_quote(self::MARKER_BEGINS, '#') . '.+' . + preg_quote(self::MARKER_ENDS, '#') . '#s'; + $baseContents = preg_replace($pattern, '', $baseContents); + if (file_put_contents($basePathTarget, $baseContents) === false) { + throw new \LogicException("Cannot update {$basePathTarget}"); + } + } + + /** + * @param array $autoGen + * @param AutogenContext $context + * @return void + */ + public function doGitIgnore(array &$autoGen, AutogenContext $context) + { + $lineAdds = $context->gitignoreAdds; + $lineAdds = array_unique($lineAdds); + GitIgnore::sort($lineAdds); + + $lineDeletes = $context->gitignoreDeletes; + $lineDeletes = array_unique($lineDeletes); + GitIgnore::sort($lineDeletes); + + $addOnDir = $context->getAddOnDirectory(); + $gitignorePath = "{$addOnDir}/.gitignore"; + + $gitignore = []; + $changed = false; + if (file_exists($gitignorePath)) { + $currentLines = file($gitignorePath); + if (!is_array($currentLines)) { + $currentLines = []; + } + $currentLines = array_map('trim', $currentLines); + foreach ($currentLines as $currentLine) { + if (in_array($currentLine, $lineDeletes, true)) { + $changed = true; + continue; + } + + $gitignore[] = $currentLine; + } + } + + foreach ($lineAdds as $line) { + if (in_array($line, $gitignore, true)) { + continue; + } + + $gitignore[] = $line; + $changed = true; + } + + if ($changed) { + GitIgnore::sort($gitignore); + + if (!File::writeFile($gitignorePath, implode("\n", $gitignore), false)) { + $context->writeln("Cannot update {$gitignorePath}"); + } else { + $context->writeln( + "{$gitignorePath} OK", + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + } + } + + if (isset($autoGen['.gitignore'])) { + unset($autoGen['.gitignore']); + } + } + + /** + * @return void + */ + protected function configure() + { + $this + ->setName('devhelper:autogen') + ->setDescription('Generate code/template/phrase/etc. automatically for the specified add-on') + ->addArgument( + 'id', + InputArgument::REQUIRED, + 'Add-On ID' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $id = $input->getArgument('id'); + $addOn = $this->checkEditableAddOn($id, $error); + if (!$addOn) { + $output->writeln('' . $error . ''); + return 1; + } + + $this->assertDevHelperLatest($output); + $autoGen = $this->autoGenLoad($addOn, $output); + $autoGenBefore = md5(serialize($autoGen)); + + $context = new AutogenContext($this, $input, $output, \XF::app(), $addOn); + $this->doAdminControllerEntity($autoGen, $context); + $this->doGitIgnore($autoGen, $context); + + $autoGen[__CLASS__]['version_id'] = self::VERSION_ID; + if (isset($autoGen[__CLASS__]['time'])) { + // `time` was used in previous version, remove it now + // TODO: remove this around next year? (August 2019) + unset($autoGen[__CLASS__]['time']); + } + + ksort($autoGen); + $autoGenAfter = md5(serialize($autoGen)); + if ($autoGenAfter !== $autoGenBefore) { + $autoGenPath = $this->autoGenPath($addOn); + if (!File::writeFile($autoGenPath, Json::jsonEncodePretty($autoGen), false)) { + throw new \LogicException("Cannot update {$autoGenPath}"); + } + + $output->writeln("autogen@" . $autoGen[__CLASS__]['version_id']); + } else { + $output->writeln("autogen OK"); + } + + return 0; + } + + /** + * @param string $path + * @param string $contents + * @param array $autoGen + * @return void + */ + private function extractVersion($path, $contents, array &$autoGen) + { + if (preg_match('#\n \* @version (\d+)\n#', $contents, $versionMatches) !== 1) { + throw new \LogicException("Cannot extract autogen version from {$path}"); + } + $autoGen[$path] = intval($versionMatches[1]); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc3a332 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/r/xfrocks/xenforo/tags/ +FROM xfrocks/xenforo:php-apache-7.3.12c + +# https://packagist.org/packages/phpstan/phpstan +# https://packagist.org/packages/phpstan/phpstan-strict-rules +RUN composer global require \ + phpstan/phpstan:0.12.38 \ + phpstan/phpstan-strict-rules:0.12.4 \ + && mv /tmp/vendor /etc/devhelper-composer-vendor + +ENV DEVHELPER_PHP_APACHE_VERSION_ID 2020082201 + +COPY docker/build.sh /tmp/build.sh +RUN chmod +x /tmp/build.sh && /tmp/build.sh + +WORKDIR "/var/www/html/src/addons" diff --git a/PHPStan/PhpDoc/DumbUnionTypeNodeResolver.php b/PHPStan/PhpDoc/DumbUnionTypeNodeResolver.php new file mode 100644 index 0000000..d34fdeb --- /dev/null +++ b/PHPStan/PhpDoc/DumbUnionTypeNodeResolver.php @@ -0,0 +1,40 @@ +typeNodeResolver = $typeNodeResolver; + } + + public function getCacheKey(): string + { + return __CLASS__ . '-2019031301'; + } + + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type + { + if ($typeNode instanceof UnionTypeNode) { + // override PHPStan's default smart union type resolver with our simplified version + // the default one merge iterable types together with lots of logic + // which do not match XenForo type hint usages -> we have to disable it completely + $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); + return TypeCombinator::union(...$types); + } + + return null; + } +} diff --git a/PHPStan/Reflection/EntityColumnReflection.php b/PHPStan/Reflection/EntityColumnReflection.php new file mode 100644 index 0000000..4c3c150 --- /dev/null +++ b/PHPStan/Reflection/EntityColumnReflection.php @@ -0,0 +1,129 @@ +declaringClass = $declaringClass; + $this->type = $type; + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function getDocComment(): ?string + { + return null; + } + + /** + * @return Type + * @throws \PHPStan\ShouldNotHappenException + */ + public function getReadableType(): Type + { + switch ($this->type) { + case Entity::INT: + case Entity::UINT: + return new IntegerType(); + case Entity::FLOAT: + return new FloatType(); + case Entity::BOOL: + return new BooleanType(); + case Entity::STR: + case Entity::BINARY: + return new StringType(); + case Entity::SERIALIZED: + case Entity::JSON: + return new UnionType([new ArrayType(new MixedType(), new MixedType()), new BooleanType()]); + case Entity::SERIALIZED_ARRAY: + case Entity::JSON_ARRAY: + case Entity::LIST_LINES: + case Entity::LIST_COMMA: + return new ArrayType(new MixedType(), new MixedType()); + } + + throw new \PHPStan\ShouldNotHappenException(); + } + + /** + * @return Type + * @throws \PHPStan\ShouldNotHappenException + */ + public function getWritableType(): Type + { + return $this->getReadableType(); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isStatic(): bool + { + return false; + } + + public function isWritable(): bool + { + return true; + } +} diff --git a/PHPStan/Reflection/EntityGetterReflection.php b/PHPStan/Reflection/EntityGetterReflection.php new file mode 100644 index 0000000..db44d1d --- /dev/null +++ b/PHPStan/Reflection/EntityGetterReflection.php @@ -0,0 +1,98 @@ +declaringClass = $declaringClass; + $this->type = $type; + $this->isWritable = $isWritable; + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getReadableType(): Type + { + return $this->type; + } + + public function getWritableType(): Type + { + return $this->type; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isStatic(): bool + { + return false; + } + + public function isWritable(): bool + { + return $this->isWritable; + } +} diff --git a/PHPStan/Reflection/EntityRelationReflection.php b/PHPStan/Reflection/EntityRelationReflection.php new file mode 100644 index 0000000..6ecf5bc --- /dev/null +++ b/PHPStan/Reflection/EntityRelationReflection.php @@ -0,0 +1,112 @@ +declaringClass = $declaringClass; + $this->type = $type; + $this->entity = $entity; + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getReadableType(): Type + { + $className = str_replace(':', '\Entity\\', $this->entity); + + if ($this->type === Entity::TO_ONE) { + return TypeCombinator::addNull(new ObjectType($className)); + } else { + return new ArrayType(new MixedType(), new ObjectType($className)); + } + } + + public function getWritableType(): Type + { + return $this->getReadableType(); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isStatic(): bool + { + return false; + } + + public function isWritable(): bool + { + return false; + } +} diff --git a/PHPStan/Type/Entity.php b/PHPStan/Type/Entity.php new file mode 100644 index 0000000..eb39ba9 --- /dev/null +++ b/PHPStan/Type/Entity.php @@ -0,0 +1,113 @@ +getStructure($classReflection); + if ($structure === null) { + return false; + } + + if (isset($structure->columns[$propertyName])) { + return true; + } + if (isset($structure->getters[$propertyName])) { + return true; + } + if (isset($structure->relations[$propertyName])) { + return true; + } + + if (substr($propertyName, -1) === '_') { + $propertyNameWithoutDash = substr($propertyName, 0, -1); + if (isset($structure->columns[$propertyNameWithoutDash])) { + return true; + } + } + + return false; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + $structure = $this->getStructure($classReflection); + if ($structure === null) { + throw new \PHPStan\ShouldNotHappenException(); + } + + if (isset($structure->getters[$propertyName])) { + if (isset($structure->getters[$propertyName]['getter'])) { + $methodName = $structure->getters[$propertyName]['getter']; + } else { + $methodName = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $propertyName))); + } + if ($classReflection->hasNativeMethod($methodName)) { + $method = $classReflection->getNativeMethod($methodName); + return new EntityGetterReflection( + $classReflection, + $method->getVariants()[0]->getReturnType(), + isset($structure->columns[$propertyName]) + ); + } + } + + if (isset($structure->columns[$propertyName])) { + $column = $structure->columns[$propertyName]; + return new EntityColumnReflection($classReflection, $column['type']); + } + + if (isset($structure->relations[$propertyName])) { + $relation = $structure->relations[$propertyName]; + return new EntityRelationReflection($classReflection, $relation['type'], $relation['entity']); + } + + if (substr($propertyName, -1) === '_') { + $propertyNameWithoutDash = substr($propertyName, 0, -1); + if (isset($structure->columns[$propertyNameWithoutDash])) { + $column = $structure->columns[$propertyNameWithoutDash]; + return new EntityColumnReflection($classReflection, $column['type']); + } + } + + throw new \PHPStan\ShouldNotHappenException(); + } + + protected function getStructure(ClassReflection $classReflection): ?Structure + { + static $structures = []; + $className = $classReflection->getName(); + if (isset($structures[$className])) { + return $structures[$className]; + } + + $isEntity = $classReflection->isSubclassOf('XF\Mvc\Entity\Entity'); + if (!$isEntity) { + return null; + } + + $structure = null; + $classNameGetStructure = [$className, 'getStructure']; + if (is_callable($classNameGetStructure)) { + try { + $_structure = new Structure(); + call_user_func($classNameGetStructure, $_structure); + $structures[$className] = $structure = $_structure; + } catch (\Exception $e) { + // ignore + } + } + + return $structure; + } +} diff --git a/PHPStan/autoload.php b/PHPStan/autoload.php new file mode 100644 index 0000000..3ccc13d --- /dev/null +++ b/PHPStan/autoload.php @@ -0,0 +1,68 @@ +extension(); + $reflection = new ReflectionClass($extension); + $property = $reflection->getProperty('classExtensions'); + $property->setAccessible(true); + $classExtensions = $property->getValue($extension); + } + + foreach ($classExtensions as $base => $extensions) { + foreach ($extensions as $extension) { + if ($extension === $classWithoutPrefix) { + class_alias($base, $class); + return true; + } + } + } + + return null; +}); + +return XF::$autoLoader; diff --git a/PHPStan/phpstan.neon b/PHPStan/phpstan.neon new file mode 100644 index 0000000..faf0a40 --- /dev/null +++ b/PHPStan/phpstan.neon @@ -0,0 +1,41 @@ +includes: + - /etc/devhelper-composer-vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + checkMissingIterableValueType: false + + excludes_analyse: + - */_output/* + - */_releases/* + - */vendor/* + + ignoreErrors: + - '#^If condition is always false.$#' + - '#^Parameter \#1 \$categories of method XF\\Repository\\AbstractCategoryTree::createCategoryTree\(\) expects null#' + - '#^Parameter \#1 \$toSerialize of static method XF\\Util\\Php::safeSerialize\(\) expects string#' + - '#^Parameter \#2 \$keyedBy of method XF\\Mvc\\Entity\\Finder::pluckFrom\(\) expects null#' + - '#^Parameter \#2 \$type of method XF\\Mvc\\Controller::filter\(\) expects string\|null#' + - '#^PHPDoc tag @property has invalid value#' + - '#^Undefined variable: \$escape$#' + - '#\\Controller\\.+::action.+ has no return typehint specified.#' + - '#\\Entity\\.+::_(post|pre)Save\(\)+ has no return typehint specified.#' + - '#::installStep\d+\(\) has no return typehint specified.#' + - '#::uninstallStep\d+\(\) has no return typehint specified.#' + - '#::upgrade\d+Step\d+\(\) has no return typehint specified.#' + + reportUnmatchedIgnoredErrors: false + + universalObjectCratesClasses: + - ArrayObject + - XF\Mvc\ParameterBag + - XF\Payment\CallbackState + +services: + - + class: DevHelper\PHPStan\PhpDoc\DumbUnionTypeNodeResolver + tags: + - phpstan.phpDoc.typeNodeResolverExtension + - + class: DevHelper\PHPStan\Type\Entity + tags: + - phpstan.broker.propertiesClassReflectionExtension diff --git a/README.md b/README.md deleted file mode 100644 index 8feb9a7..0000000 --- a/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# XenForo Developer Helper add-on - -## Installation -### The Easy Way -Just copy files and import the xml file as usual. - -If you want to help developing this add-on. It's recommended to clone the repo elsewhere then create symbolic links for `js/DevHelper` and `library/DevHelper`. -### Advanced Installation -Edit XenForo `index.php` and `admin.php` files in root directory. Look for these lines: - -``` -require($fileDir . '/library/XenForo/Autoloader.php'); -XenForo_Autoloader::getInstance()->setupAutoloader($fileDir . '/library'); -``` - -Replace them with: - -``` -require($fileDir . '/library/XenForo/Autoloader.php'); -require($fileDir . '/library/DevHelper/Autoloader.php'); -DevHelper_Autoloader::getDevHelperInstance()->setupAutoloader($fileDir . '/library'); -``` -This is required to use features like ShippableHelper injection, debug information in AJAX requests, etc. - -## Features - * Quickly setup add-on environment (UNIX only) with command line `xf-new-addon addOnId path/to/xenforo/root`. This will create a new directory `addOnId` in the current directory with the below layout. All inner `addOnId` directories will be symlink'd to their places within `path/to/xenforo/root` making it easy for you to start coding. All files should be put within one of these 3 directories. - -``` -addOnId - | - |--repo - | - |--js - | - |--addOnId - |--library - | - |--addOnId - |--styles - | - |--default - | - |--addOnId -``` - - * __Data Manager__ with flexible configuration. It can generates DataWriter, Model, Route PrefixAdmin, ControllerAdmin for you. See __Configuration__ for detailed information. - * __Generate Installer___ (and uninstaller too) using data from __Data Manager__. - * __File Export__ to copy clean files from working directory to another directory for easy package. Added benefits include: - * Auto generated `FileSums.php` - * Phrase check (unused phrases, deleted phrases, phrases from other add-ons) - * Auto generated `XFCP` classes for better IDE support (class hint, method hint, etc.) - *. Template editors - -## Configuration -The first time you visit __Data Manager__ for an add-on, a new file will be generated in `library/addOnId/DevHelper/Config.php` and it looks like this: - -``` -addDataPatch( - 'xf_user', - array( - 'name' => 'money', - 'type' => 'uint', - 'default' => 0, - ) - ); - } -} -``` - -When the `Config` file is upgraded, it will look like this: - -``` - array( - 'addonid_money' => array('name' => 'addonid_money', 'type' => 'uint', 'default' => 0), - ), - ); - - protected function _upgrade() - { - return true; // remove this line to trigger update - } -} -``` - -And if you generate installer from the AdminCP, you will get this `Installer` file: - -``` - 'xf_user', - 'field' => 'addonid_money', - 'showTablesQuery' => 'SHOW TABLES LIKE \'xf_user\'', - 'showColumnsQuery' => 'SHOW COLUMNS FROM `xf_user` LIKE \'addonid_money\'', - 'alterTableAddColumnQuery' => 'ALTER TABLE `xf_user` ADD COLUMN `addonid_money` INT(10) UNSIGNED DEFAULT \'0\'', - 'alterTableDropColumnQuery' => 'ALTER TABLE `xf_user` DROP COLUMN `addonid_money`', - ), - ); - - public static function install($existingAddOn, $addOnData) - { - $db = XenForo_Application::get('db'); - - foreach (self::$_patches as $patch) { - $tableExisted = $db->fetchOne($patch['showTablesQuery']); - if (empty($tableExisted)) { - continue; - } - - $existed = $db->fetchOne($patch['showColumnsQuery']); - if (empty($existed)) { - $db->query($patch['alterTableAddColumnQuery']); - } - } - } - - public static function uninstall() - { - $db = XenForo_Application::get('db'); - - foreach (self::$_patches as $patch) { - $tableExisted = $db->fetchOne($patch['showTablesQuery']); - if (empty($tableExisted)) { - continue; - } - - $existed = $db->fetchOne($patch['showColumnsQuery']); - if (!empty($existed)) { - $db->query($patch['alterTableDropColumnQuery']); - } - } - } -} -``` \ No newline at end of file diff --git a/Util/Autogen/AdminNav.php b/Util/Autogen/AdminNav.php new file mode 100644 index 0000000..9866dbf --- /dev/null +++ b/Util/Autogen/AdminNav.php @@ -0,0 +1,64 @@ +finder('XF:AdminNavigation') + ->where('navigation_id', $navId) + ->fetchOne(); + if ($existing !== null) { + $context->writeln( + "Admin navigation {$existing->navigation_id} OK", + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + return $existing; + } + + /** @var AdminNavigation $newAdminNav */ + $newAdminNav = $context->createEntity('XF:AdminNavigation'); + $newAdminNav->navigation_id = $navId; + $newAdminNav->link = $link; + $newAdminNav->addon_id = $context->getAddOnId(); + + Phrase::autogen($context, $newAdminNav->getPhraseName()); + + $parentNavId = 'addOns'; + $questionText = sprintf('Enter parent for admin nav %s [%s]: ', $navId, $parentNavId); + $question = new Question($questionText, $parentNavId); + $parentNavId = $context->ask($question); + $parentNav = self::autogen($context, $parentNavId); + $newAdminNav->parent_navigation_id = $parentNav->navigation_id; + + if ($parentNav->addon_id !== $context->getAddOnId()) { + $newAdminNav->display_order = 9999; + } else { + $displayOrder = 0; + foreach ($parentNav->Children as $siblingNav) { + $displayOrder = max($displayOrder, $siblingNav->display_order); + } + $newAdminNav->display_order = $displayOrder + 10; + } + + $newAdminNav->save(); + $context->writeln("Admin navigation {$newAdminNav->navigation_id} NEW"); + + return $newAdminNav; + } +} diff --git a/Util/Autogen/AdminRoute.php b/Util/Autogen/AdminRoute.php new file mode 100644 index 0000000..2e44a55 --- /dev/null +++ b/Util/Autogen/AdminRoute.php @@ -0,0 +1,58 @@ +finder('XF:Route') + ->where('route_type', 'admin') + ->where('route_prefix', $routePrefix) + ->where('addon_id', $context->getAddOnId()) + ->fetchOne(); + if ($existing !== null) { + $context->writeln( + "Route #{$existing->route_id} {$existing->route_type}/{$existing->route_prefix} OK", + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + return $existing; + } + + /** @var Route $newRoute */ + $newRoute = $context->createEntity('XF:Route'); + $newRoute->route_type = 'admin'; + $newRoute->route_prefix = $routePrefix; + $newRoute->format = sprintf(':int<%s>/', $primaryKey); + $newRoute->controller = $controller; + $newRoute->addon_id = $context->getAddOnId(); + + $questionText = sprintf('Enter context for route %s: ', $routePrefix); + $question = new Question($questionText); + $routeContext = $context->ask($question); + if (is_string($routeContext)) { + $adminNav = \DevHelper\Util\Autogen\AdminNav::autogen($context, $routeContext, $routePrefix); + $newRoute->context = $adminNav->navigation_id; + } + + $newRoute->save(); + $context->writeln("Route #{$newRoute->route_id} {$newRoute->route_type}/{$newRoute->route_prefix} NEW"); + + return $newRoute; + } +} diff --git a/Util/Autogen/AdminTemplate.php b/Util/Autogen/AdminTemplate.php new file mode 100644 index 0000000..91a66c2 --- /dev/null +++ b/Util/Autogen/AdminTemplate.php @@ -0,0 +1,69 @@ +gitignoreDeletes[] = sprintf('/_output/templates/admin/%s.html', $titleTarget); + + /** @var Template|null $templateSource */ + $templateSource = $context->finder('XF:Template') + ->where('type', 'admin') + ->where('style_id', 0) + ->where('addon_id', 'DevHelper') + ->where('title', $titleSource) + ->fetchOne(); + if ($templateSource === null) { + throw new \LogicException("Source template {$titleSource} not found"); + } + + /** @var Template|null $templateTarget */ + $templateTarget = $context->finder('XF:Template') + ->where('type', 'admin') + ->where('style_id', 0) + ->where('addon_id', $context->getAddOnId()) + ->where('title', $titleTarget) + ->fetchOne(); + + if ($templateTarget === null) { + /** @var Template $newTemplate */ + $newTemplate = $context->createEntity('XF:Template'); + $newTemplate->type = 'admin'; + $newTemplate->title = $titleTarget; + $newTemplate->style_id = 0; + $newTemplate->template = $templateSource->template; + $newTemplate->addon_id = $context->getAddOnId(); + $newTemplate->save(); + + $context->writeln("Template #{$newTemplate->template_id} {$newTemplate->title} NEW"); + return $newTemplate; + } + + if ($templateTarget->template === $templateSource->template) { + $context->writeln( + "Template #{$templateTarget->template_id} {$templateTarget->title} OK", + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE + ); + return $templateTarget; + } else { + $templateTarget->template = $templateSource->template; + $templateTarget->save(); + + $context->writeln("Template #{$templateTarget->template_id} {$templateTarget->title} UPDATED"); + return $templateTarget; + } + } +} diff --git a/Util/Autogen/GitIgnore.php b/Util/Autogen/GitIgnore.php new file mode 100644 index 0000000..931260b --- /dev/null +++ b/Util/Autogen/GitIgnore.php @@ -0,0 +1,20 @@ +finder('XF:Phrase') + ->where('language_id', 0) + ->where('addon_id', $context->getAddOnId()) + ->where('title', $title) + ->fetchOne(); + if ($existing !== null) { + $context->writeln( + "Phrase #{$existing->phrase_id} {$existing->title} = {$existing->phrase_text} OK", + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + return $existing; + } + + /** @var EntityPhrase $newPhrase */ + $newPhrase = $context->createEntity('XF:Phrase'); + $newPhrase->addon_id = $context->getAddOnId(); + $newPhrase->language_id = 0; + $newPhrase->title = $title; + + $questionText = sprintf('Enter phrase text for %s: ', $title); + $question = new Question($questionText); + $newPhrase->phrase_text = $context->ask($question); + + $newPhrase->save(); + $context->writeln("Phrase #{$newPhrase->phrase_id} {$newPhrase->title} = {$newPhrase->phrase_text} NEW"); + + return $newPhrase; + } +} diff --git a/Util/AutogenContext.php b/Util/AutogenContext.php new file mode 100644 index 0000000..f4e8de7 --- /dev/null +++ b/Util/AutogenContext.php @@ -0,0 +1,158 @@ +command = $command; + $this->input = $input; + $this->output = $output; + $this->app = $app; + $this->addOn = $addOn; + } + + /** + * @param Question $question + * @return mixed + */ + public function ask($question) + { + /** @var QuestionHelper $helper */ + $helper = $this->command->getHelper('question'); + return $helper->ask($this->input, $this->output, $question); + } + + /** + * @param string $shortName + * @return Entity + */ + public function createEntity($shortName) + { + return $this->app->em()->create($shortName); + } + + /** + * @param mixed $obj + * @return bool + * @see \DevHelper\Autogen\Admin\Controller\Entity::devHelperAutogen() + */ + public function executeDevHelperAutogen($obj) + { + $f = [$obj, 'devHelperAutogen']; + if (!is_callable($f)) { + return false; + } + + call_user_func($f, $this); + return true; + } + + /** + * @param string $identifier + * @return Finder + */ + public function finder($identifier) + { + return $this->app->finder($identifier); + } + + /** + * @return string + */ + public function getAddOnId() + { + return $this->addOn->getAddOnId(); + } + + /** + * @return string + */ + public function getAddOnDirectory() + { + return $this->addOn->getAddOnDirectory(); + } + + /** + * @param string $class + * @return Controller + */ + public function newController($class) + { + return new $class($this->app, $this->app->request()); + } + + /** + * @param string|array $messages + * @param int $options + * @return void + */ + public function writeln($messages, $options = 0) + { + $this->output->writeln($messages, $options); + } +} diff --git a/Util/ZipArchiveToDir.php b/Util/ZipArchiveToDir.php new file mode 100644 index 0000000..db1e53b --- /dev/null +++ b/Util/ZipArchiveToDir.php @@ -0,0 +1,79 @@ +dir = rtrim($dir, DIRECTORY_SEPARATOR); + + if (is_dir($this->dir)) { + throw new \InvalidArgumentException('Directory already exists: ' . $this->dir); + } + + \XF\Util\File::createDirectory($this->dir, false); + } + + /** + * @param string $dirName + * @return bool + */ + public function addEmptyDir($dirName) + { + return \XF\Util\File::createDirectory($this->getPath($dirName), false); + } + + /** + * @param string $fileName + * @param string $localName + * @return bool + */ + public function addFile($fileName, $localName) + { + return \XF\Util\File::renameFile($fileName, $this->getPath($localName), false); + } + + /** + * @return bool + */ + public function close() + { + if (isset($GLOBALS['runner']) && + $GLOBALS['runner'] instanceof \XF\Cli\Runner + ) { + $output = new ConsoleOutput(); + $output->writeln(sprintf('%s->dir = %s', __CLASS__, var_export($this->dir, true))); + } + + return true; + } + + /** + * @return string + */ + public function getStatusString() + { + return $this->dir; + } + + /** + * @param string $name + * @return string + */ + protected function getPath($name) + { + $ds = DIRECTORY_SEPARATOR; + return $this->dir . $ds . ltrim($name, $ds); + } +} diff --git a/XF/AddOn/Manager.php b/XF/AddOn/Manager.php new file mode 100644 index 0000000..1300c6c --- /dev/null +++ b/XF/AddOn/Manager.php @@ -0,0 +1,64 @@ + $addOn) { + if ($addOn->hasMissingFiles()) { + continue; + } + + $filtered[$addOnId] = $addOn; + } + + return $filtered; + } + + /** + * @param \XF\AddOn\AddOn $addOn + * @return array + */ + public function getDevHelperConfig($addOn) + { + $path = $this->getDevHelperConfigJsonPath($addOn); + if (!file_exists($path)) { + return []; + } + + $json = file_get_contents($path); + if ($json === false) { + return []; + } + + $config = json_decode($json, true); + if (!is_array($config)) { + return []; + } + + return $config; + } + + /** + * @param \XF\AddOn\AddOn $addOn + * @return string + */ + public function getDevHelperConfigJsonPath($addOn) + { + return $addOn->getFilesDirectory() . DIRECTORY_SEPARATOR . 'dev' . DIRECTORY_SEPARATOR . 'config.json'; + } +} + +// phpcs:disable +if (false) { + class XFCP_Manager extends \XF\AddOn\Manager + { + } +} diff --git a/XF/DevelopmentOutput.php b/XF/DevelopmentOutput.php new file mode 100644 index 0000000..f6e8aaf --- /dev/null +++ b/XF/DevelopmentOutput.php @@ -0,0 +1,48 @@ +returnEnabledInsteadOfAvailableAddOnIds > 0) { + $addOns = \XF::app()->container('addon.cache'); + $addOnIds = array_keys($addOns); + $addOnIds = array_filter($addOnIds, function ($id) { + return $id !== 'XF'; + }); + return $addOnIds; + } + + return parent::getAvailableAddOnIds(); + } + + /** + * @param mixed $typeDir + * @return void + */ + protected function loadTypeMetadata($typeDir) + { + $this->returnEnabledInsteadOfAvailableAddOnIds++; + + parent::loadTypeMetadata($typeDir); + + $this->returnEnabledInsteadOfAvailableAddOnIds--; + } +} + +// phpcs:disable +if (false) { + class XFCP_DevelopmentOutput extends \XF\DevelopmentOutput + { + } +} diff --git a/XF/Entity/AddOn.php b/XF/Entity/AddOn.php new file mode 100644 index 0000000..60029c1 --- /dev/null +++ b/XF/Entity/AddOn.php @@ -0,0 +1,34 @@ +isUpdate() && $this->isChanged('active') && $this->active) { + /** @var Manager $addOnManager */ + $addOnManager = $this->app()->addOnManager(); + $config = $addOnManager->getDevHelperConfig($addOnManager->getById($this->addon_id)); + if (!isset($config[Manager::CONFIG_ADDON_IDS_AUTO_ENABLE])) { + return; + } + + foreach ($config[Manager::CONFIG_ADDON_IDS_AUTO_ENABLE] as $addOnId) { + /** @var \XF\Entity\AddOn|null $addOn */ + $addOn = $this->_em->find('XF:AddOn', $addOnId); + if ($addOn !== null && !$addOn->active) { + $addOn->active = true; + $this->addCascadedSave($addOn); + } + } + } + } +} diff --git a/XF/Entity/ClassExtension.php b/XF/Entity/ClassExtension.php new file mode 100644 index 0000000..a15d52d --- /dev/null +++ b/XF/Entity/ClassExtension.php @@ -0,0 +1,79 @@ +to_class === '') { + $newClass = $this->generateToClassFileAutomatically(); + if ($newClass !== '') { + $this->to_class = $newClass; + } + } + } + + /** + * @param mixed $class + * @return bool + */ + protected function verifyToClass(&$class) + { + if ($class === '') { + return true; + } + + return parent::verifyToClass($class); + } + + /** + * @return string + */ + protected function generateToClassFileAutomatically() + { + $newClass = str_replace('/', '\\', $this->addon_id) . '\\' . $this->from_class; + + $addOn = $this->app()->addOnManager()->getById($this->addon_id); + if ($addOn === null) { + return ''; + } + + $addOnDirPath = $addOn->getAddOnDirectory(); + $newClassPath = $addOnDirPath . '/' . str_replace('\\', '/', $this->from_class) . '.php'; + if (file_exists($newClassPath)) { + return $newClass; + } + + $newClassParts = explode('\\', $newClass); + $newClassName = array_pop($newClassParts); + $newNamespace = implode('\\', $newClassParts); + $newClassContents = <<container('addon.cache'); + $foundAddOnId = ''; + + foreach ($addOns as $addOnId => $versionId) { + if (substr($withoutPrefix, 0, strlen($addOnId)) !== $addOnId) { + continue; + } + + if (strlen($addOnId) > strlen($foundAddOnId)) { + $foundAddOnId = $addOnId; + } + } + + if ($foundAddOnId !== '') { + $assetPath = "/var/www/html/src/addons/{$foundAddOnId}/_files/$routePath"; + if (file_exists($assetPath)) { + $mimeType = mime_content_type($assetPath); + $contents = file_get_contents($assetPath); + + header("X-Add-On-Id: {$foundAddOnId}"); + header("X-Asset-Path: {$assetPath}"); + if ($mimeType !== false) { + header("Content-Type: {$mimeType}"); + } + die($contents); + } + } + + header('HTTP/1.0 404 Not Found'); + echo("Unable to resolve add-on file for `{$routePath}`
\n"); + echo("Please make sure that the add-on is enabled and its files are put into `_files/styles/default/addOnId/` directory.
\n"); + exit; + } + + return parent::route($routePath); + } +} + +// phpcs:disable +if (false) { + class XFCP_Dispatcher extends \XF\Mvc\Dispatcher + { + } +} diff --git a/XF/Service/AddOn/ReleaseBuilder.php b/XF/Service/AddOn/ReleaseBuilder.php new file mode 100644 index 0000000..f52280c --- /dev/null +++ b/XF/Service/AddOn/ReleaseBuilder.php @@ -0,0 +1,51 @@ +zipArchiveToDir = strval(getenv('DEVHELPER_ZIP_ARCHIVE_TO_DIR')); + if ($this->zipArchiveToDir !== '') { + $ds = DIRECTORY_SEPARATOR; + $dir = $this->addOn->getReleasesDirectory() . $ds . ltrim($this->zipArchiveToDir, $ds); + + /** @var \ZipArchive $zipArchiveToDir */ + $zipArchiveToDir = new ZipArchiveToDir($dir); + $this->zipArchive = $zipArchiveToDir; + } + } + + /** + * @return void + */ + public function finalizeRelease() + { + parent::finalizeRelease(); + + if ($this->zipArchiveToDir !== '') { + $dir = $this->zipArchive->getStatusString(); + $list = ['src/addons/' . $this->addOn->getAddOnId()]; + + $additionalFiles = $this->addOn->offsetGet('additional_files'); + if (is_array($additionalFiles)) { + $list = array_merge($list, $additionalFiles); + } + + file_put_contents($dir . DIRECTORY_SEPARATOR . 'list.txt', implode("\n", $list) . "\n"); + } + } +} diff --git a/_output/class_extensions/XF-AddOn-Manager_DevHelper-XF-AddOn-Manager.json b/_output/class_extensions/XF-AddOn-Manager_DevHelper-XF-AddOn-Manager.json new file mode 100644 index 0000000..10baf8c --- /dev/null +++ b/_output/class_extensions/XF-AddOn-Manager_DevHelper-XF-AddOn-Manager.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\AddOn\\Manager", + "to_class": "DevHelper\\XF\\AddOn\\Manager", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/XF-DevelopmentOutput_DevHelper-XF-DevelopmentOutput.json b/_output/class_extensions/XF-DevelopmentOutput_DevHelper-XF-DevelopmentOutput.json new file mode 100644 index 0000000..8ace90f --- /dev/null +++ b/_output/class_extensions/XF-DevelopmentOutput_DevHelper-XF-DevelopmentOutput.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\DevelopmentOutput", + "to_class": "DevHelper\\XF\\DevelopmentOutput", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/XF-Entity-AddOn_DevHelper-XF-Entity-AddOn.json b/_output/class_extensions/XF-Entity-AddOn_DevHelper-XF-Entity-AddOn.json new file mode 100644 index 0000000..47e8e5b --- /dev/null +++ b/_output/class_extensions/XF-Entity-AddOn_DevHelper-XF-Entity-AddOn.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\Entity\\AddOn", + "to_class": "DevHelper\\XF\\Entity\\AddOn", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/XF-Entity-ClassExtension_DevHelper-XF-Entity-ClassExtension.json b/_output/class_extensions/XF-Entity-ClassExtension_DevHelper-XF-Entity-ClassExtension.json new file mode 100644 index 0000000..1c45244 --- /dev/null +++ b/_output/class_extensions/XF-Entity-ClassExtension_DevHelper-XF-Entity-ClassExtension.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\Entity\\ClassExtension", + "to_class": "DevHelper\\XF\\Entity\\ClassExtension", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/XF-Mvc-Dispatcher_DevHelper-XF-Mvc-Dispatcher.json b/_output/class_extensions/XF-Mvc-Dispatcher_DevHelper-XF-Mvc-Dispatcher.json new file mode 100644 index 0000000..2d10804 --- /dev/null +++ b/_output/class_extensions/XF-Mvc-Dispatcher_DevHelper-XF-Mvc-Dispatcher.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\Mvc\\Dispatcher", + "to_class": "DevHelper\\XF\\Mvc\\Dispatcher", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/XF-Service-AddOn-ReleaseBuilder_DevHelper-XF-Service-AddOn-ReleaseBuilder.json b/_output/class_extensions/XF-Service-AddOn-ReleaseBuilder_DevHelper-XF-Service-AddOn-ReleaseBuilder.json new file mode 100644 index 0000000..f73e2a2 --- /dev/null +++ b/_output/class_extensions/XF-Service-AddOn-ReleaseBuilder_DevHelper-XF-Service-AddOn-ReleaseBuilder.json @@ -0,0 +1,6 @@ +{ + "from_class": "XF\\Service\\AddOn\\ReleaseBuilder", + "to_class": "DevHelper\\XF\\Service\\AddOn\\ReleaseBuilder", + "execute_order": 10, + "active": true +} \ No newline at end of file diff --git a/_output/class_extensions/_metadata.json b/_output/class_extensions/_metadata.json new file mode 100644 index 0000000..933a80d --- /dev/null +++ b/_output/class_extensions/_metadata.json @@ -0,0 +1,20 @@ +{ + "XF-AddOn-Manager_DevHelper-XF-AddOn-Manager.json": { + "hash": "e0f1377d34957ba5d55ec229db8b5c51" + }, + "XF-DevelopmentOutput_DevHelper-XF-DevelopmentOutput.json": { + "hash": "513733893e24d900ae9250cf0b5c7a23" + }, + "XF-Entity-AddOn_DevHelper-XF-Entity-AddOn.json": { + "hash": "7b0e2ceec240c176a793d35ef7368c2d" + }, + "XF-Entity-ClassExtension_DevHelper-XF-Entity-ClassExtension.json": { + "hash": "c8931e2a04123ee3735d20a60e8ab606" + }, + "XF-Mvc-Dispatcher_DevHelper-XF-Mvc-Dispatcher.json": { + "hash": "b2b9c0743c9822ce7caccd244c0b01a2" + }, + "XF-Service-AddOn-ReleaseBuilder_DevHelper-XF-Service-AddOn-ReleaseBuilder.json": { + "hash": "1d2909d7cd5967fd2f0a124f1d84ff0a" + } +} \ No newline at end of file diff --git a/_output/extension_hint.php b/_output/extension_hint.php new file mode 100644 index 0000000..1b798d6 --- /dev/null +++ b/_output/extension_hint.php @@ -0,0 +1,30 @@ +{{ phrase('confirm_action') }} + + +
+
+ + {{ phrase('please_confirm_that_you_want_to_delete_following:') }} + + + {$entityLabel} + + {$entityLabel} + + + +
+ +
+
\ No newline at end of file diff --git a/_output/templates/admin/devhelper_autogen_ace_edit.html b/_output/templates/admin/devhelper_autogen_ace_edit.html new file mode 100644 index 0000000..45d00d1 --- /dev/null +++ b/_output/templates/admin/devhelper_autogen_ace_edit.html @@ -0,0 +1,101 @@ + + {$phrases.add|raw} + + {$phrases.edit|raw}: {$controller.getEntityLabel($entity)} + + + + + + + +
+ +
+ + + + + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ phrase('you_may_use_html') }} + + + + + + + + + + + + + \ No newline at end of file diff --git a/_output/templates/admin/devhelper_autogen_ace_list.html b/_output/templates/admin/devhelper_autogen_ace_list.html new file mode 100644 index 0000000..7ae3bde --- /dev/null +++ b/_output/templates/admin/devhelper_autogen_ace_list.html @@ -0,0 +1,84 @@ +{$phrases.entities|raw} + + + + {$phrases.add|raw} + + + + + + + + +
+ +
+ + + + + +
+
+ +
+
+ + + + + + {{ phrase('more_records_matching_filter_more_specific') }} + + + + +
+
+ + +
+ +
{{ phrase('no_items_have_been_created_yet') }}
+
+ + + + + + + + + + + + + + {$popup|raw} + + + + + \ No newline at end of file diff --git a/addon.json b/addon.json new file mode 100644 index 0000000..d619a6e --- /dev/null +++ b/addon.json @@ -0,0 +1,14 @@ +{ + "legacy_addon_id": "", + "title": "DevHelper", + "description": "", + "version_id": 2000011, + "version_string": "2.0.0 Alpha 1", + "dev": "", + "dev_url": "", + "faq_url": "", + "support_url": "", + "extra_urls": [], + "require": [], + "icon": "" +} \ No newline at end of file diff --git a/aptana.xml b/aptana.xml deleted file mode 100644 index d94a118..0000000 --- a/aptana.xml +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev-scripts/Dockerfile.php-apache b/dev-scripts/Dockerfile.php-apache deleted file mode 100644 index f43b292..0000000 --- a/dev-scripts/Dockerfile.php-apache +++ /dev/null @@ -1,17 +0,0 @@ -FROM xfrocks/xenforo:php-apache-7.1.8 - -RUN apt-get update && apt-get install -y \ - npm \ - && a2enmod rewrite \ - && npm install uglify-js -g \ - && pear install PHP_CodeSniffer \ - && rm -rf /var/lib/apt/lists/* - -RUN pecl install apcu \ - && docker-php-ext-enable apcu - -COPY docker/* /usr/local/bin/ -RUN chmod +x /usr/local/bin/cmd.sh \ - && chmod +x /usr/local/bin/find-addons.sh - -CMD ["/usr/local/bin/cmd.sh"] diff --git a/dev-scripts/docker/cmd.sh b/dev-scripts/docker/cmd.sh deleted file mode 100644 index 3633003..0000000 --- a/dev-scripts/docker/cmd.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -e - -find-addons.sh - -exec apache2-foreground diff --git a/dev-scripts/docker/find-addons.sh b/dev-scripts/docker/find-addons.sh deleted file mode 100644 index bd5c8f4..0000000 --- a/dev-scripts/docker/find-addons.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -e - -cd /var/www/html/ - -{ \ - echo "# Generated by $0"; \ - find addons/ -maxdepth 5 -name library -type d | grep -v .git | sed 's#/library$##'; \ -} > ./xenforo/internal_data/addons.txt diff --git a/dev-scripts/helpers/create.php b/dev-scripts/helpers/create.php deleted file mode 100755 index 2cd5607..0000000 --- a/dev-scripts/helpers/create.php +++ /dev/null @@ -1,71 +0,0 @@ - $filePath) - { - echo " $helperFunction\n"; - } - } - else - { - echo "Requesting " . $PARAMS[0] . "\n"; - } -} -else -{ - if (count($PARAMS) <= 1) - { - $candidates += array_keys($GLOBALS['HELPERS']); - } -} \ No newline at end of file diff --git a/dev-scripts/helpers/quit.php b/dev-scripts/helpers/quit.php deleted file mode 100755 index df70233..0000000 --- a/dev-scripts/helpers/quit.php +++ /dev/null @@ -1,5 +0,0 @@ -getAddOnOptionsList(false, false)); - } - - public static function loadCachedModel($class) - { - static $models = array(); - - if (!isset($models[$class])) - { - $models[$class] = XenForo_Model::create($class); - } - - return $models[$class]; - } - - public static function parseCommand($command) - { - $parts = explode(' ', preg_replace('/\s+/', ' ', $command)); - - foreach (array_keys($parts) as $i) - { - if ($parts[$i] === '') - { - unset($parts[$i]); - } - } - - return $parts; - } -} \ No newline at end of file diff --git a/dev-scripts/xf-compatibility-check.sh b/dev-scripts/xf-compatibility-check.sh deleted file mode 100644 index d09ac54..0000000 --- a/dev-scripts/xf-compatibility-check.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/php -setupAutoloader($pathToXenForoRoot . '/library'); -XenForo_Application::initialize($pathToXenForoRoot . '/library', $pathToXenForoRoot); - -function checkFile($file) -{ - $contents = file_get_contents($file); - $offset = 0; - - while(true) - { - if (preg_match('/extends[^X]+(XFCP_[A-Za-z_]+)/', $contents, $matches, PREG_OFFSET_CAPTURE, $offset)) - { - $offset = $matches[0][1] + 1; - - $xfcpClassName = $matches[1][0]; - $parts = explode('_', $xfcpClassName); - array_shift($parts); // remove XFCP_ - array_shift($parts); // remove add-on prefix (if add-on has multiple prefixes, this is good enough to prevent processing our own file) - - $processingParts = array(); - $processed = false; - while (count($parts) > 0) - { - $part = array_pop($parts); - array_unshift($processingParts, $part); - $processingClassName = implode('_', $processingParts); - - if (class_exists($processingClassName)) - { - eval('class ' . $xfcpClassName . ' extends ' . $processingClassName . ' {}'); - - try - { - require($file); - } - catch (Exception $e) - { - echo sprintf("Exception: %s\n", $e->getMessage()); - } - - $processed = true; - } - elseif (strpos($processingClassName, 'XenForo_ViewPublic_') === 0) - { - // for XenForo_ViewPublic_* classes, if the view class itself cannot be found - // the system will load XenForo_ViewPublic_Base, we will check againts that now - eval('class ' . $xfcpClassName . ' extends XenForo_ViewPublic_Base {}'); - - try - { - require($file); - } - catch (Exception $e) - { - echo sprintf("Exception: %s\n", $e->getMessage()); - } - - $processed = true; - } - - if ($processed) - { - break; - } - } - } - else - { - break; - } - } -} - -function loop($dir) -{ - $nodes = glob(sprintf("%s/*", $dir)); - - foreach($nodes as $node) - { - if (is_dir($node)) - { - loop($node); - } - else - { - $ext = strtolower(pathinfo($node, PATHINFO_EXTENSION)); - if ($ext === 'php') - { - checkFile($node); - } - } - } -} -loop($pathToAddOnRepo); \ No newline at end of file diff --git a/dev-scripts/xf-find-new-addons.sh b/dev-scripts/xf-find-new-addons.sh deleted file mode 100755 index d48912f..0000000 --- a/dev-scripts/xf-find-new-addons.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -_xfPath=$1 -_repoPath=$2 - -if [ ! -d "$_xfPath" ]; then - echo "XenForo Path $_xfPath not found" >&2 - exit 1 -fi - -if [ ! -d "$_repoPath" ]; then - echo "Repo Path $_repoPath not found" >&2 - exit 1 -fi - -_extractVersionId() { - _filePath=$1 - _line=$(head -n 2 "$_filePath" | grep -o 'version_id="\d*"' ) - echo "$_line" - exit 1 -} - -find "$_xfPath" -type f -name 'addon-*.xml' | while read -r _xfFilePath ; do - _fileName=$( basename "$_xfFilePath" ) - _addOnId=$( echo "$_fileName" | sed 's/^addon-//' | sed 's/\.xml$//' ) - _xfVersionId=$( _extractVersionId "$_xfFilePath" ) - - find "$_repoPath" -type f -name "$_fileName" | while read -r _repoFilePath ; do - _repoVersionId=$( _extractVersionId "$_repoFilePath" ) - if [ "$_xfVersionId" != "$_repoVersionId" ]; then - echo "$_addOnId:\n\tXenForo $_xfVersionId\n\tRepo $_repoVersionId" - fi - done -done diff --git a/dev-scripts/xf-helper.php b/dev-scripts/xf-helper.php deleted file mode 100755 index b50ab56..0000000 --- a/dev-scripts/xf-helper.php +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/php -setupAutoloader($PWD . '/library'); -XenForo_Application::initialize($PWD . '/library', $PWD); - -// load our functions -@include($DIR . '/helpers_includes/common.php'); -if (!class_exists('Helper_Common')) -{ - echo "Corrupted script\n"; - exit(-1); -} - -function readline_completion_impl($string, $index) -{ - $readline_info = readline_info(); - $line = substr($readline_info['line_buffer'], 0, $readline_info['end']); - $parts = Helper_Common::parseCommand($line); - $candidates = array(); - - if (empty($parts)) - { - // no input yet, just return list of helper functions - $candidates += array_keys($GLOBALS['HELPERS']); - } - else - { - if (isset($GLOBALS['HELPERS'][$parts[0]])) - { - // we actually got the helper function correctly - $PARAMS = array_slice($parts, 1); - $IS_COMPLETION = true; - require($GLOBALS['HELPERS'][$parts[0]]); - } - else - { - // incomplete helper function... - $candidates += array_keys($GLOBALS['HELPERS']); - } - } - - return $candidates; -} -readline_completion_function('readline_completion_impl'); - -while (true) -{ - $command = readline('> '); - readline_add_history($command); - $parts = Helper_Common::parseCommand($command); - - if (empty($parts[0]) OR !isset($HELPERS[$parts[0]])) - { - $parts[0] = 'help'; - } - - $PARAMS = array_slice($parts, 1); - require($HELPERS[$parts[0]]); -} \ No newline at end of file diff --git a/dev-scripts/xf-new-addon.sh b/dev-scripts/xf-new-addon.sh deleted file mode 100755 index 2bb94f1..0000000 --- a/dev-scripts/xf-new-addon.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -set -e - -if [ $# -ge 2 ]; then - _addOnDirRelativePath="$1" - _xenforoRootDirPath="$2" - - _addOnDirRelativePath=$( echo $_addOnDirRelativePath | sed 's#_#/#' | sed 's#/$##' ) - if [ -d "${_addOnDirRelativePath}/repo" ]; then - # legacy support - _repoDirRelativePath="${_addOnDirRelativePath}/repo" - else - _repoDirRelativePath="${_addOnDirRelativePath}" - fi - - _xenforoMajorVersion=1 - _phpRelativePath="" - _xenforoRootDirPath=$( echo $_xenforoRootDirPath | sed 's#/$##' ) - if [ -d "${_xenforoRootDirPath}" ]; then - if [ -d "${_xenforoRootDirPath}/src/addons/" ]; then - _xenforoMajorVersion=2 - echo "XenForo 2 detected" - _phpRelativePath="src/addons/${_addOnDirRelativePath}" - else - if [ -d "${_xenforoRootDirPath}/library/" ]; then - echo "XenForo 1 detected" - _phpRelativePath="library/${_addOnDirRelativePath}" - else - echo './library or ./src/addons does not exists!' >&2 - exit 1 - fi - fi - else - echo "${_xenforoRootDirPath} does not exists!" >&2 - exit 1 - fi - - _doSubPath() { - _subPath=$1 - _path="${PWD}/${_repoDirRelativePath}/${_subPath}" - _gitignorePath="${_xenforoRootDirPath}/.gitignore" - _xenforoPath="${_xenforoRootDirPath}/${_subPath}" - - if [ ! -d "${_path}" ]; then - mkdir -p "${_path}" - fi - - if [ ! -e "${_xenforoPath}" ]; then - _xenforoPathDirPath="$( dirname "${_xenforoPath}" )" - if [ ! -d "${_xenforoPathDirPath}" ]; then - echo "mkdir ${_xenforoPathDirPath}..." - sudo mkdir -p "${_xenforoPathDirPath}" - fi - - echo "ln ${_path} ${_xenforoPath}..." - sudo ln -s "${_path}" "${_xenforoPath}" - sudo chown -h $USER "${_xenforoPath}" - if [ -f "${_gitignorePath}" ]; then - echo "/${_subPath}" >> "${_gitignorePath}" - fi - fi - - echo "Sub path ${_subPath} ok" - } - _doSubPath "${_phpRelativePath}" - _doSubPath "js/${_addOnDirRelativePath}" - _doSubPath "styles/default/${_addOnDirRelativePath}" - - if [ ! -e "${_repoDirRelativePath}/.git" ]; then - git init -- "${_repoDirRelativePath}" - echo "git init ${_repoDirRelativePath} ok" - fi -else - echo USAGE: $0 [addOnId] [path/to/xenforo/root] -fi diff --git a/dev-scripts/xf-setup-root.sh b/dev-scripts/xf-setup-root.sh deleted file mode 100644 index ab1b134..0000000 --- a/dev-scripts/xf-setup-root.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -PATH_TO_XENFORO=`pwd` -PATH_TO_REPO=$(cd `dirname "${BASH_SOURCE[0]}"` && cd ../../.. && pwd) - -# install DevHelper if haven't done that yet -cd $PATH_TO_REPO && $PATH_TO_REPO/DevHelper/repo/dev-scripts/xf-new-addon.sh DevHelper $PATH_TO_XENFORO - -if [ ! -e "$PATH_TO_XENFORO/docker-compose.override.yml" ]; then - ln -s $PATH_TO_REPO/DevHelper/repo/docker-compose.yml $PATH_TO_XENFORO/docker-compose.yml - ( \ - echo "version: '2'"; \ - echo ""; \ - echo "services:"; \ - echo " php-apache:"; \ - echo " ports:"; \ - echo " - "$RANDOM:80""; \ - ) > $PATH_TO_XENFORO/docker-compose.override.yml -fi diff --git a/docker-compose.yml b/docker-compose.yml index 4edfd67..a069329 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,55 +1,26 @@ version: '2' services: - php-apache: + php: build: - context: dev-scripts - dockerfile: Dockerfile.php-apache - environment: - - VIRTUAL_HOST=xenforo.local.xfrocks.com - expose: - - "80" - links: - - mysqld:mysqld - #volumes: - #- /path/to/xenforo:/var/www/html/xenforo:rw - #- /path/to/addons:/var/www/html/addons:rw - #- .:/var/www/html/:rw - - php-cli: - image: xfrocks/docker-xenforo:php-cli + context: . links: - - php-apache:php-apache - - mysqld:mysqld - volumes_from: - - php-apache - working_dir: /var/www/html/ + - mysql:mysql + ports: + - "80:80" + volumes: + - ./../../xenforo:/var/www/html + - ./..:/var/www/html/src/addons + - ./../../xenforo/src/addons/XF:/var/www/html/src/addons/XF + - ./.data/external:/var/www/html/data + - ./.data/internal:/var/www/html/internal_data - mysqld: + mysql: image: mysql:latest environment: - MYSQL_RANDOM_ROOT_PASSWORD: 'yes' - MYSQL_ONETIME_PASSWORD: 'yes' - MYSQL_DATABASE: 'xenforo' - MYSQL_USER: 'xenforo' - MYSQL_PASSWORD: 'xenforo' + MYSQL_DATABASE: 'xenforo2' + MYSQL_ROOT_PASSWORD: 'root' expose: - "3306" volumes: - - ./.data/mysql:/var/lib/mysql:rw - - mysql: - image: arey/mysql-client - links: - - mysqld:mysqld - volumes_from: - - php-apache - working_dir: /var/www/html/ - entrypoint: mysql -hmysqld -uxenforo -pxenforo xenforo - - nginx-proxy: - image: jwilder/nginx-proxy - ports: - - "80:80" - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro \ No newline at end of file + - ./.data/mysql:/var/lib/mysql diff --git a/docker/apache.conf b/docker/apache.conf new file mode 100644 index 0000000..ac390f6 --- /dev/null +++ b/docker/apache.conf @@ -0,0 +1,7 @@ +ErrorLog ${APACHE_LOG_DIR}/error.log +CustomLog ${APACHE_LOG_DIR}/access.log combined +DocumentRoot /var/www/html + + + FallbackResource /index.php + diff --git a/docker/build.sh b/docker/build.sh new file mode 100644 index 0000000..cd9f7db --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +echo 'PassEnv DEVHELPER_PHP_APACHE_VERSION_ID' >> /etc/apache2/mods-available/env.conf +a2enmod env rewrite + +{ \ + echo ''; \ + echo ' include /var/www/html/src/addons/DevHelper/docker/apache.conf'; \ + echo ''; \ +} > /etc/apache2/sites-enabled/000-default.conf + +{ \ + echo ''; \ + echo ' include /var/www/html/src/addons/DevHelper/docker/apache.conf'; \ + echo ' SSLEngine on'; \ + echo ' SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem'; \ + echo ' SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key'; \ + echo ''; \ +} > /etc/apache2/sites-enabled/default-ssl.conf + +echo 'export "PATH=$PATH:/var/www/html/src/addons/DevHelper/docker"' >> /root/.bashrc + +{ \ + echo 'auto_prepend_file=/var/www/html/src/addons/DevHelper/prepend.php'; \ + echo 'max_execution_time=-1'; \ +} > /usr/local/etc/php/conf.d/DevHelper.ini + +for _verb in build-release \ + bump-version \ + disable \ + enable \ + install \ + install-step \ + rebuild \ + sync-json \ + uninstall \ + uninstall-step \ + upgrade \ + upgrade-step \ + validate-json \ + ; do + _verbPath="/usr/local/bin/xf-addon--${_verb}" + { + echo '#!/bin/bash'; \ + echo; \ + echo 'set -e'; \ + echo; \ + echo "exec cmd-php--xf-addon.sh ${_verb} \"\$@\""; + } >"${_verbPath}" + chmod +x "${_verbPath}" +done + +rm -rf /tmp/* diff --git a/docker/cmd-php--xf-addon.sh b/docker/cmd-php--xf-addon.sh new file mode 100755 index 0000000..5702cee --- /dev/null +++ b/docker/cmd-php--xf-addon.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +_verb="${1}" +if [ -z "${_verb}" ]; then + echo 'Verb is missing' >&2 + exit 1 +fi + +_addOnId="${2%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 2 +fi + +shift 2 + +exec cmd-php.sh "xf-addon:${_verb}" "${_addOnId}" "$@" diff --git a/docker/cmd-php.sh b/docker/cmd-php.sh new file mode 100755 index 0000000..bebe938 --- /dev/null +++ b/docker/cmd-php.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +exec php /var/www/html/cmd.php "$@" diff --git a/docker/devhelper-autocheck.sh b/docker/devhelper-autocheck.sh new file mode 100755 index 0000000..10eafd6 --- /dev/null +++ b/docker/devhelper-autocheck.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +_addOnId="${1%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 1 +fi + +exec cmd-php.sh devhelper:autocheck "${_addOnId}" diff --git a/docker/devhelper-autogen.sh b/docker/devhelper-autogen.sh new file mode 100755 index 0000000..f823ff9 --- /dev/null +++ b/docker/devhelper-autogen.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +_addOnId="${1%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 1 +fi + +exec cmd-php.sh devhelper:autogen "${_addOnId}" diff --git a/docker/devhelper-build-release-to-dir.sh b/docker/devhelper-build-release-to-dir.sh new file mode 100755 index 0000000..f2ca3cb --- /dev/null +++ b/docker/devhelper-build-release-to-dir.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +_addOnId="${1}" +_dir="${2}" + +if [ -z "$_addOnId" -o -z "$_dir" ]; then + echo Usage: devhelper--build-release-to-dir Vendor/AddOnId dir_name + exit 1 +fi + +export DEVHELPER_ZIP_ARCHIVE_TO_DIR="${_dir}" + +exec xf-addon--build-release "${_addOnId}" diff --git a/docker/devhelper-phpcbf.sh b/docker/devhelper-phpcbf.sh new file mode 100755 index 0000000..3cf7d91 --- /dev/null +++ b/docker/devhelper-phpcbf.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +export PHPCBF=1 + +exec devhelper-phpcs.sh "$@" diff --git a/docker/devhelper-phpcs.sh b/docker/devhelper-phpcs.sh new file mode 100755 index 0000000..056c068 --- /dev/null +++ b/docker/devhelper-phpcs.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +_phpcsXmlPath='/var/www/html/src/addons/DevHelper/phpcs.xml' +set -- "--standard=${_phpcsXmlPath}" "$@" + +if [ -z "$PHPCBF" ]; then + set -- phpcs -s "$@" +else + set -- phpcbf "$@" +fi + +exec "$@" diff --git a/docker/devhelper-phpstan.sh b/docker/devhelper-phpstan.sh new file mode 100755 index 0000000..be09f40 --- /dev/null +++ b/docker/devhelper-phpstan.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -e + +_srcPath="${1%/}" +if [ -z "${_srcPath}" ]; then + echo 'Source path is missing' >&2 + exit 1 +fi + +_checkPath=${2:-${_srcPath}} + +_neonPath='/var/www/html/src/addons/DevHelper/PHPStan/phpstan.neon' +_srcNeonPath="${_srcPath}/_files/dev/phpstan.neon" +if [ -f ${_srcNeonPath} ]; then + _neonPath=${_srcNeonPath} + echo "Using ${_neonPath}" >&2 +fi + +export "DEVHELPER_PHPSTAN_SRC_PATH=${_srcPath}" + +echo "Running PHPStan against ${_checkPath}..." +exec /etc/devhelper-composer-vendor/bin/phpstan analyse \ + --autoload-file=/var/www/html/src/addons/DevHelper/PHPStan/autoload.php \ + --level max \ + --memory-limit=-1 \ + -c ${_neonPath} \ + ${_checkPath} diff --git a/docker/xf-addon--create.sh b/docker/xf-addon--create.sh new file mode 100755 index 0000000..9c67d55 --- /dev/null +++ b/docker/xf-addon--create.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +exec cmd-php.sh xf-addon:create diff --git a/docker/xf-addon--export.sh b/docker/xf-addon--export.sh new file mode 100755 index 0000000..4d349c3 --- /dev/null +++ b/docker/xf-addon--export.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +_addOnId="${1%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 1 +fi + +echo 'xf-addon--export.sh is no longer functional,' +echo "Use \`cmd-php--xf-addon.sh export ${_addOnId}\` if you have to..." +echo "For other usages, please use \`xf-dev--export--addon.sh ${_addOnId}\` instead." +exit 1 diff --git a/docker/xf-dev--entity-class-properties.sh b/docker/xf-dev--entity-class-properties.sh new file mode 100755 index 0000000..21ba48a --- /dev/null +++ b/docker/xf-dev--entity-class-properties.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +_addOnOrEntity="${1%/}" +if [ -z "${_addOnOrEntity}" ]; then + echo 'Add-on or entity is missing' >&2 + exit 1 +fi + +exec cmd-php.sh xf-dev:entity-class-properties "${_addOnOrEntity}" diff --git a/docker/xf-dev--export--addon.sh b/docker/xf-dev--export--addon.sh new file mode 100755 index 0000000..138f797 --- /dev/null +++ b/docker/xf-dev--export--addon.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +_addOnId="${1%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 1 +fi + +devhelper-autogen.sh "${_addOnId}" +devhelper-autocheck.sh "${_addOnId}" + +echo 'Running phpcs, it may take a while...' +_addOnDir="/var/www/html/src/addons/${_addOnId}" +_phpcs=$( devhelper-phpcs.sh "${_addOnDir}" 2>&1 || true ) +if [ ! -z "$_phpcs" ]; then + echo "$_phpcs" + + _phpcbfSuggestion=$( echo "$_phpcs" | grep 'PHPCBF CAN FIX' ) + if [ ! -z "$_phpcbfSuggestion" ]; then + echo "phpcs failed, execute \`devhelper-phpcbf.sh ${_addOnDir}\` to attempt fixing automatically" >&2 + exit 2 + fi + + echo 'phpcs failed' >&2 + exit 1 +fi +echo 'phpcs OK' + +# Silently enable the add-on because PHPStan has to resolve XFCP classes +xf-addon--enable --no-interaction "${_addOnId}" >/dev/null 2>&1 + +devhelper-phpstan.sh "${_addOnDir}" + +exec cmd-php.sh xf-dev:export --addon "${_addOnId}" diff --git a/docker/xf-dev--import--addon.sh b/docker/xf-dev--import--addon.sh new file mode 100755 index 0000000..64e3892 --- /dev/null +++ b/docker/xf-dev--import--addon.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +_addOnId="${1%/}" +if [ -z "${_addOnId}" ]; then + echo 'Add-on ID is missing' >&2 + exit 1 +fi + +exec cmd-php.sh xf-dev:import --addon "${_addOnId}" diff --git a/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.css b/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.css deleted file mode 100644 index 677c078..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.css +++ /dev/null @@ -1,32 +0,0 @@ -.CodeMirror-dialog { - position: absolute; - left: 0; right: 0; - background: inherit; - z-index: 15; - padding: .1em .8em; - overflow: hidden; - color: inherit; -} - -.CodeMirror-dialog-top { - border-bottom: 1px solid #eee; - top: 0; -} - -.CodeMirror-dialog-bottom { - border-top: 1px solid #eee; - bottom: 0; -} - -.CodeMirror-dialog input { - border: none; - outline: none; - background: transparent; - width: 20em; - color: inherit; - font-family: monospace; -} - -.CodeMirror-dialog button { - font-size: 70%; -} diff --git a/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.js b/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.js deleted file mode 100644 index f10bb5b..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/dialog/dialog.js +++ /dev/null @@ -1,157 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// Open simple dialogs on top of an editor. Relies on dialog.css. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - function dialogDiv(cm, template, bottom) { - var wrap = cm.getWrapperElement(); - var dialog; - dialog = wrap.appendChild(document.createElement("div")); - if (bottom) - dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; - else - dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; - - if (typeof template == "string") { - dialog.innerHTML = template; - } else { // Assuming it's a detached DOM element. - dialog.appendChild(template); - } - return dialog; - } - - function closeNotification(cm, newVal) { - if (cm.state.currentNotificationClose) - cm.state.currentNotificationClose(); - cm.state.currentNotificationClose = newVal; - } - - CodeMirror.defineExtension("openDialog", function(template, callback, options) { - if (!options) options = {}; - - closeNotification(this, null); - - var dialog = dialogDiv(this, template, options.bottom); - var closed = false, me = this; - function close(newVal) { - if (typeof newVal == 'string') { - inp.value = newVal; - } else { - if (closed) return; - closed = true; - dialog.parentNode.removeChild(dialog); - me.focus(); - - if (options.onClose) options.onClose(dialog); - } - } - - var inp = dialog.getElementsByTagName("input")[0], button; - if (inp) { - inp.focus(); - - if (options.value) { - inp.value = options.value; - if (options.selectValueOnOpen !== false) { - inp.select(); - } - } - - if (options.onInput) - CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); - if (options.onKeyUp) - CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); - - CodeMirror.on(inp, "keydown", function(e) { - if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } - if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { - inp.blur(); - CodeMirror.e_stop(e); - close(); - } - if (e.keyCode == 13) callback(inp.value, e); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); - } else if (button = dialog.getElementsByTagName("button")[0]) { - CodeMirror.on(button, "click", function() { - close(); - me.focus(); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); - - button.focus(); - } - return close; - }); - - CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { - closeNotification(this, null); - var dialog = dialogDiv(this, template, options && options.bottom); - var buttons = dialog.getElementsByTagName("button"); - var closed = false, me = this, blurring = 1; - function close() { - if (closed) return; - closed = true; - dialog.parentNode.removeChild(dialog); - me.focus(); - } - buttons[0].focus(); - for (var i = 0; i < buttons.length; ++i) { - var b = buttons[i]; - (function(callback) { - CodeMirror.on(b, "click", function(e) { - CodeMirror.e_preventDefault(e); - close(); - if (callback) callback(me); - }); - })(callbacks[i]); - CodeMirror.on(b, "blur", function() { - --blurring; - setTimeout(function() { if (blurring <= 0) close(); }, 200); - }); - CodeMirror.on(b, "focus", function() { ++blurring; }); - } - }); - - /* - * openNotification - * Opens a notification, that can be closed with an optional timer - * (default 5000ms timer) and always closes on click. - * - * If a notification is opened while another is opened, it will close the - * currently opened one and open the new one immediately. - */ - CodeMirror.defineExtension("openNotification", function(template, options) { - closeNotification(this, close); - var dialog = dialogDiv(this, template, options && options.bottom); - var closed = false, doneTimer; - var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; - - function close() { - if (closed) return; - closed = true; - clearTimeout(doneTimer); - dialog.parentNode.removeChild(dialog); - } - - CodeMirror.on(dialog, 'click', function(e) { - CodeMirror.e_preventDefault(e); - close(); - }); - - if (duration) - doneTimer = setTimeout(close, duration); - - return close; - }); -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/scroll/annotatescrollbar.js b/js/DevHelper/Lib/CodeMirror/addon/scroll/annotatescrollbar.js deleted file mode 100644 index f2276fc..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/scroll/annotatescrollbar.js +++ /dev/null @@ -1,122 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("annotateScrollbar", function(options) { - if (typeof options == "string") options = {className: options}; - return new Annotation(this, options); - }); - - CodeMirror.defineOption("scrollButtonHeight", 0); - - function Annotation(cm, options) { - this.cm = cm; - this.options = options; - this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); - this.annotations = []; - this.doRedraw = this.doUpdate = null; - this.div = cm.getWrapperElement().appendChild(document.createElement("div")); - this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; - this.computeScale(); - - function scheduleRedraw(delay) { - clearTimeout(self.doRedraw); - self.doRedraw = setTimeout(function() { self.redraw(); }, delay); - } - - var self = this; - cm.on("refresh", this.resizeHandler = function() { - clearTimeout(self.doUpdate); - self.doUpdate = setTimeout(function() { - if (self.computeScale()) scheduleRedraw(20); - }, 100); - }); - cm.on("markerAdded", this.resizeHandler); - cm.on("markerCleared", this.resizeHandler); - if (options.listenForChanges !== false) - cm.on("change", this.changeHandler = function() { - scheduleRedraw(250); - }); - } - - Annotation.prototype.computeScale = function() { - var cm = this.cm; - var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / - cm.getScrollerElement().scrollHeight - if (hScale != this.hScale) { - this.hScale = hScale; - return true; - } - }; - - Annotation.prototype.update = function(annotations) { - this.annotations = annotations; - this.redraw(); - }; - - Annotation.prototype.redraw = function(compute) { - if (compute !== false) this.computeScale(); - var cm = this.cm, hScale = this.hScale; - - var frag = document.createDocumentFragment(), anns = this.annotations; - - var wrapping = cm.getOption("lineWrapping"); - var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; - var curLine = null, curLineObj = null; - function getY(pos, top) { - if (curLine != pos.line) { - curLine = pos.line; - curLineObj = cm.getLineHandle(curLine); - } - if ((curLineObj.widgets && curLineObj.widgets.length) || - (wrapping && curLineObj.height > singleLineH)) - return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; - var topY = cm.heightAtLine(curLineObj, "local"); - return topY + (top ? 0 : curLineObj.height); - } - - var lastLine = cm.lastLine() - if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { - var ann = anns[i]; - if (ann.to.line > lastLine) continue; - var top = nextTop || getY(ann.from, true) * hScale; - var bottom = getY(ann.to, false) * hScale; - while (i < anns.length - 1) { - if (anns[i + 1].to.line > lastLine) break; - nextTop = getY(anns[i + 1].from, true) * hScale; - if (nextTop > bottom + .9) break; - ann = anns[++i]; - bottom = getY(ann.to, false) * hScale; - } - if (bottom == top) continue; - var height = Math.max(bottom - top, 3); - - var elt = frag.appendChild(document.createElement("div")); - elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " - + (top + this.buttonHeight) + "px; height: " + height + "px"; - elt.className = this.options.className; - if (ann.id) { - elt.setAttribute("annotation-id", ann.id); - } - } - this.div.textContent = ""; - this.div.appendChild(frag); - }; - - Annotation.prototype.clear = function() { - this.cm.off("refresh", this.resizeHandler); - this.cm.off("markerAdded", this.resizeHandler); - this.cm.off("markerCleared", this.resizeHandler); - if (this.changeHandler) this.cm.off("change", this.changeHandler); - this.div.parentNode.removeChild(this.div); - }; -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/scroll/scrollpastend.js b/js/DevHelper/Lib/CodeMirror/addon/scroll/scrollpastend.js deleted file mode 100644 index a2ed089..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/scroll/scrollpastend.js +++ /dev/null @@ -1,48 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.off("change", onChange); - cm.off("refresh", updateBottomMargin); - cm.display.lineSpace.parentNode.style.paddingBottom = ""; - cm.state.scrollPastEndPadding = null; - } - if (val) { - cm.on("change", onChange); - cm.on("refresh", updateBottomMargin); - updateBottomMargin(cm); - } - }); - - function onChange(cm, change) { - if (CodeMirror.changeEnd(change).line == cm.lastLine()) - updateBottomMargin(cm); - } - - function updateBottomMargin(cm) { - var padding = ""; - if (cm.lineCount() > 1) { - var totalH = cm.display.scroller.clientHeight - 30, - lastLineH = cm.getLineHandle(cm.lastLine()).height; - padding = (totalH - lastLineH) + "px"; - } - if (cm.state.scrollPastEndPadding != padding) { - cm.state.scrollPastEndPadding = padding; - cm.display.lineSpace.parentNode.style.paddingBottom = padding; - cm.off("refresh", updateBottomMargin); - cm.setSize(); - cm.on("refresh", updateBottomMargin); - } - } -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.css b/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.css deleted file mode 100644 index 5eea7aa..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.css +++ /dev/null @@ -1,66 +0,0 @@ -.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { - position: absolute; - background: #ccc; - -moz-box-sizing: border-box; - box-sizing: border-box; - border: 1px solid #bbb; - border-radius: 2px; -} - -.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { - position: absolute; - z-index: 6; - background: #eee; -} - -.CodeMirror-simplescroll-horizontal { - bottom: 0; left: 0; - height: 8px; -} -.CodeMirror-simplescroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-simplescroll-vertical { - right: 0; top: 0; - width: 8px; -} -.CodeMirror-simplescroll-vertical div { - right: 0; - width: 100%; -} - - -.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { - display: none; -} - -.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { - position: absolute; - background: #bcd; - border-radius: 3px; -} - -.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { - position: absolute; - z-index: 6; -} - -.CodeMirror-overlayscroll-horizontal { - bottom: 0; left: 0; - height: 6px; -} -.CodeMirror-overlayscroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-overlayscroll-vertical { - right: 0; top: 0; - width: 6px; -} -.CodeMirror-overlayscroll-vertical div { - right: 0; - width: 100%; -} diff --git a/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.js b/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.js deleted file mode 100644 index 23f3e03..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/scroll/simplescrollbars.js +++ /dev/null @@ -1,152 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function Bar(cls, orientation, scroll) { - this.orientation = orientation; - this.scroll = scroll; - this.screen = this.total = this.size = 1; - this.pos = 0; - - this.node = document.createElement("div"); - this.node.className = cls + "-" + orientation; - this.inner = this.node.appendChild(document.createElement("div")); - - var self = this; - CodeMirror.on(this.inner, "mousedown", function(e) { - if (e.which != 1) return; - CodeMirror.e_preventDefault(e); - var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; - var start = e[axis], startpos = self.pos; - function done() { - CodeMirror.off(document, "mousemove", move); - CodeMirror.off(document, "mouseup", done); - } - function move(e) { - if (e.which != 1) return done(); - self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); - } - CodeMirror.on(document, "mousemove", move); - CodeMirror.on(document, "mouseup", done); - }); - - CodeMirror.on(this.node, "click", function(e) { - CodeMirror.e_preventDefault(e); - var innerBox = self.inner.getBoundingClientRect(), where; - if (self.orientation == "horizontal") - where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; - else - where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; - self.moveTo(self.pos + where * self.screen); - }); - - function onWheel(e) { - var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; - var oldPos = self.pos; - self.moveTo(self.pos + moved); - if (self.pos != oldPos) CodeMirror.e_preventDefault(e); - } - CodeMirror.on(this.node, "mousewheel", onWheel); - CodeMirror.on(this.node, "DOMMouseScroll", onWheel); - } - - Bar.prototype.setPos = function(pos, force) { - if (pos < 0) pos = 0; - if (pos > this.total - this.screen) pos = this.total - this.screen; - if (!force && pos == this.pos) return false; - this.pos = pos; - this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = - (pos * (this.size / this.total)) + "px"; - return true - }; - - Bar.prototype.moveTo = function(pos) { - if (this.setPos(pos)) this.scroll(pos, this.orientation); - } - - var minButtonSize = 10; - - Bar.prototype.update = function(scrollSize, clientSize, barSize) { - var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize - if (sizeChanged) { - this.screen = clientSize; - this.total = scrollSize; - this.size = barSize; - } - - var buttonSize = this.screen * (this.size / this.total); - if (buttonSize < minButtonSize) { - this.size -= minButtonSize - buttonSize; - buttonSize = minButtonSize; - } - this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = - buttonSize + "px"; - this.setPos(this.pos, sizeChanged); - }; - - function SimpleScrollbars(cls, place, scroll) { - this.addClass = cls; - this.horiz = new Bar(cls, "horizontal", scroll); - place(this.horiz.node); - this.vert = new Bar(cls, "vertical", scroll); - place(this.vert.node); - this.width = null; - } - - SimpleScrollbars.prototype.update = function(measure) { - if (this.width == null) { - var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; - if (style) this.width = parseInt(style.height); - } - var width = this.width || 0; - - var needsH = measure.scrollWidth > measure.clientWidth + 1; - var needsV = measure.scrollHeight > measure.clientHeight + 1; - this.vert.node.style.display = needsV ? "block" : "none"; - this.horiz.node.style.display = needsH ? "block" : "none"; - - if (needsV) { - this.vert.update(measure.scrollHeight, measure.clientHeight, - measure.viewHeight - (needsH ? width : 0)); - this.vert.node.style.bottom = needsH ? width + "px" : "0"; - } - if (needsH) { - this.horiz.update(measure.scrollWidth, measure.clientWidth, - measure.viewWidth - (needsV ? width : 0) - measure.barLeft); - this.horiz.node.style.right = needsV ? width + "px" : "0"; - this.horiz.node.style.left = measure.barLeft + "px"; - } - - return {right: needsV ? width : 0, bottom: needsH ? width : 0}; - }; - - SimpleScrollbars.prototype.setScrollTop = function(pos) { - this.vert.setPos(pos); - }; - - SimpleScrollbars.prototype.setScrollLeft = function(pos) { - this.horiz.setPos(pos); - }; - - SimpleScrollbars.prototype.clear = function() { - var parent = this.horiz.node.parentNode; - parent.removeChild(this.horiz.node); - parent.removeChild(this.vert.node); - }; - - CodeMirror.scrollbarModel.simple = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); - }; - CodeMirror.scrollbarModel.overlay = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); - }; -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/jump-to-line.js b/js/DevHelper/Lib/CodeMirror/addon/search/jump-to-line.js deleted file mode 100644 index 8b599cb..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/jump-to-line.js +++ /dev/null @@ -1,49 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// Defines jumpToLine command. Uses dialog.js if present. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - var jumpDialog = - 'Jump to line: (Use line:column or scroll% syntax)'; - - function interpretLine(cm, string) { - var num = Number(string) - if (/^[-+]/.test(string)) return cm.getCursor().line + num - else return num - 1 - } - - CodeMirror.commands.jumpToLine = function(cm) { - var cur = cm.getCursor(); - dialog(cm, jumpDialog, "Jump to line:", (cur.line + 1) + ":" + cur.ch, function(posStr) { - if (!posStr) return; - - var match; - if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) - } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { - var line = Math.round(cm.lineCount() * Number(match[1]) / 100); - if (/^[-+]/.test(match[1])) line = cur.line + line + 1; - cm.setCursor(line - 1, cur.ch); - } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), cur.ch); - } - }); - }; - - CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/match-highlighter.js b/js/DevHelper/Lib/CodeMirror/addon/search/match-highlighter.js deleted file mode 100644 index 73ba0e0..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/match-highlighter.js +++ /dev/null @@ -1,165 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// Highlighting text that matches the selection -// -// Defines an option highlightSelectionMatches, which, when enabled, -// will style strings that match the selection throughout the -// document. -// -// The option can be set to true to simply enable it, or to a -// {minChars, style, wordsOnly, showToken, delay} object to explicitly -// configure it. minChars is the minimum amount of characters that should be -// selected for the behavior to occur, and style is the token style to -// apply to the matches. This will be prefixed by "cm-" to create an -// actual CSS class name. If wordsOnly is enabled, the matches will be -// highlighted only if the selected text is a word. showToken, when enabled, -// will cause the current token to be highlighted when nothing is selected. -// delay is used to specify how much time to wait, in milliseconds, before -// highlighting the matches. If annotateScrollbar is enabled, the occurences -// will be highlighted on the scrollbar via the matchesonscrollbar addon. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./matchesonscrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaults = { - style: "matchhighlight", - minChars: 2, - delay: 100, - wordsOnly: false, - annotateScrollbar: false, - showToken: false, - trim: true - } - - function State(options) { - this.options = {} - for (var name in defaults) - this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] - this.overlay = this.timeout = null; - this.matchesonscroll = null; - this.active = false; - } - - CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - removeOverlay(cm); - clearTimeout(cm.state.matchHighlighter.timeout); - cm.state.matchHighlighter = null; - cm.off("cursorActivity", cursorActivity); - cm.off("focus", onFocus) - } - if (val) { - var state = cm.state.matchHighlighter = new State(val); - if (cm.hasFocus()) { - state.active = true - highlightMatches(cm) - } else { - cm.on("focus", onFocus) - } - cm.on("cursorActivity", cursorActivity); - } - }); - - function cursorActivity(cm) { - var state = cm.state.matchHighlighter; - if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) - } - - function onFocus(cm) { - var state = cm.state.matchHighlighter - if (!state.active) { - state.active = true - scheduleHighlight(cm, state) - } - } - - function scheduleHighlight(cm, state) { - clearTimeout(state.timeout); - state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); - } - - function addOverlay(cm, query, hasBoundary, style) { - var state = cm.state.matchHighlighter; - cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); - if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { - var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; - state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, - {className: "CodeMirror-selection-highlight-scrollbar"}); - } - } - - function removeOverlay(cm) { - var state = cm.state.matchHighlighter; - if (state.overlay) { - cm.removeOverlay(state.overlay); - state.overlay = null; - if (state.matchesonscroll) { - state.matchesonscroll.clear(); - state.matchesonscroll = null; - } - } - } - - function highlightMatches(cm) { - cm.operation(function() { - var state = cm.state.matchHighlighter; - removeOverlay(cm); - if (!cm.somethingSelected() && state.options.showToken) { - var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; - var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; - while (start && re.test(line.charAt(start - 1))) --start; - while (end < line.length && re.test(line.charAt(end))) ++end; - if (start < end) - addOverlay(cm, line.slice(start, end), re, state.options.style); - return; - } - var from = cm.getCursor("from"), to = cm.getCursor("to"); - if (from.line != to.line) return; - if (state.options.wordsOnly && !isWord(cm, from, to)) return; - var selection = cm.getRange(from, to) - if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") - if (selection.length >= state.options.minChars) - addOverlay(cm, selection, false, state.options.style); - }); - } - - function isWord(cm, from, to) { - var str = cm.getRange(from, to); - if (str.match(/^\w+$/) !== null) { - if (from.ch > 0) { - var pos = {line: from.line, ch: from.ch - 1}; - var chr = cm.getRange(pos, from); - if (chr.match(/\W/) === null) return false; - } - if (to.ch < cm.getLine(from.line).length) { - var pos = {line: to.line, ch: to.ch + 1}; - var chr = cm.getRange(to, pos); - if (chr.match(/\W/) === null) return false; - } - return true; - } else return false; - } - - function boundariesAround(stream, re) { - return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && - (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); - } - - function makeOverlay(query, hasBoundary, style) { - return {token: function(stream) { - if (stream.match(query) && - (!hasBoundary || boundariesAround(stream, hasBoundary))) - return style; - stream.next(); - stream.skipTo(query.charAt(0)) || stream.skipToEnd(); - }}; - } -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.css b/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.css deleted file mode 100644 index 77932cc..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.css +++ /dev/null @@ -1,8 +0,0 @@ -.CodeMirror-search-match { - background: gold; - border-top: 1px solid orange; - border-bottom: 1px solid orange; - -moz-box-sizing: border-box; - box-sizing: border-box; - opacity: .5; -} diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.js b/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.js deleted file mode 100644 index 8d19228..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/matchesonscrollbar.js +++ /dev/null @@ -1,97 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { - if (typeof options == "string") options = {className: options}; - if (!options) options = {}; - return new SearchAnnotation(this, query, caseFold, options); - }); - - function SearchAnnotation(cm, query, caseFold, options) { - this.cm = cm; - this.options = options; - var annotateOptions = {listenForChanges: false}; - for (var prop in options) annotateOptions[prop] = options[prop]; - if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; - this.annotation = cm.annotateScrollbar(annotateOptions); - this.query = query; - this.caseFold = caseFold; - this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; - this.matches = []; - this.update = null; - - this.findMatches(); - this.annotation.update(this.matches); - - var self = this; - cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); - } - - var MAX_MATCHES = 1000; - - SearchAnnotation.prototype.findMatches = function() { - if (!this.gap) return; - for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - if (match.from.line >= this.gap.to) break; - if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); - } - var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold); - var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; - while (cursor.findNext()) { - var match = {from: cursor.from(), to: cursor.to()}; - if (match.from.line >= this.gap.to) break; - this.matches.splice(i++, 0, match); - if (this.matches.length > maxMatches) break; - } - this.gap = null; - }; - - function offsetLine(line, changeStart, sizeChange) { - if (line <= changeStart) return line; - return Math.max(changeStart, line + sizeChange); - } - - SearchAnnotation.prototype.onChange = function(change) { - var startLine = change.from.line; - var endLine = CodeMirror.changeEnd(change).line; - var sizeChange = endLine - change.to.line; - if (this.gap) { - this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); - this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); - } else { - this.gap = {from: change.from.line, to: endLine + 1}; - } - - if (sizeChange) for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - var newFrom = offsetLine(match.from.line, startLine, sizeChange); - if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); - var newTo = offsetLine(match.to.line, startLine, sizeChange); - if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); - } - clearTimeout(this.update); - var self = this; - this.update = setTimeout(function() { self.updateAfterChange(); }, 250); - }; - - SearchAnnotation.prototype.updateAfterChange = function() { - this.findMatches(); - this.annotation.update(this.matches); - }; - - SearchAnnotation.prototype.clear = function() { - this.cm.off("change", this.changeHandler); - this.annotation.clear(); - }; -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/search.js b/js/DevHelper/Lib/CodeMirror/addon/search/search.js deleted file mode 100644 index 236e54c..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/search.js +++ /dev/null @@ -1,252 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// Define search commands. Depends on dialog.js or another -// implementation of the openDialog method. - -// Replace works a little oddly -- it will do the replace on the next -// Ctrl-G (or whatever is bound to findNext) press. You prevent a -// replace by making sure the match is no longer selected when hitting -// Ctrl-G. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function searchOverlay(query, caseInsensitive) { - if (typeof query == "string") - query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); - else if (!query.global) - query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); - - return {token: function(stream) { - query.lastIndex = stream.pos; - var match = query.exec(stream.string); - if (match && match.index == stream.pos) { - stream.pos += match[0].length || 1; - return "searching"; - } else if (match) { - stream.pos = match.index; - } else { - stream.skipToEnd(); - } - }}; - } - - function SearchState() { - this.posFrom = this.posTo = this.lastQuery = this.query = null; - this.overlay = null; - } - - function getSearchState(cm) { - return cm.state.search || (cm.state.search = new SearchState()); - } - - function queryCaseInsensitive(query) { - return typeof query == "string" && query == query.toLowerCase(); - } - - function getSearchCursor(cm, query, pos) { - // Heuristic: if the query string is all lowercase, do a case insensitive search. - return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true}); - } - - function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { - cm.openDialog(text, onEnter, { - value: deflt, - selectValueOnOpen: true, - closeOnEnter: false, - onClose: function() { clearSearch(cm); }, - onKeyDown: onKeyDown - }); - } - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - function confirmDialog(cm, text, shortText, fs) { - if (cm.openConfirm) cm.openConfirm(text, fs); - else if (confirm(shortText)) fs[0](); - } - - function parseString(string) { - return string.replace(/\\(.)/g, function(_, ch) { - if (ch == "n") return "\n" - if (ch == "r") return "\r" - return ch - }) - } - - function parseQuery(query) { - var isRE = query.match(/^\/(.*)\/([a-z]*)$/); - if (isRE) { - try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } - catch(e) {} // Not a regular expression after all, do a string search - } else { - query = parseString(query) - } - if (typeof query == "string" ? query == "" : query.test("")) - query = /x^/; - return query; - } - - var queryDialog = - 'Search: (Use /re/ syntax for regexp search)'; - - function startSearch(cm, state, query) { - state.queryText = query; - state.query = parseQuery(query); - cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); - state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); - cm.addOverlay(state.overlay); - if (cm.showMatchesOnScrollbar) { - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); - } - } - - function doSearch(cm, rev, persistent, immediate) { - var state = getSearchState(cm); - if (state.query) return findNext(cm, rev); - var q = cm.getSelection() || state.lastQuery; - if (persistent && cm.openDialog) { - var hiding = null - var searchNext = function(query, event) { - CodeMirror.e_stop(event); - if (!query) return; - if (query != state.queryText) { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - } - if (hiding) hiding.style.opacity = 1 - findNext(cm, event.shiftKey, function(_, to) { - var dialog - if (to.line < 3 && document.querySelector && - (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && - dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) - (hiding = dialog).style.opacity = .4 - }) - }; - persistentDialog(cm, queryDialog, q, searchNext, function(event, query) { - var keyName = CodeMirror.keyName(event) - var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName] - if (!cmd) cmd = cm.getOption('extraKeys')[keyName] - if (cmd == "findNext" || cmd == "findPrev" || - cmd == "findPersistentNext" || cmd == "findPersistentPrev") { - CodeMirror.e_stop(event); - startSearch(cm, getSearchState(cm), query); - cm.execCommand(cmd); - } else if (cmd == "find" || cmd == "findPersistent") { - CodeMirror.e_stop(event); - searchNext(query, event); - } - }); - if (immediate && q) { - startSearch(cm, state, q); - findNext(cm, rev); - } - } else { - dialog(cm, queryDialog, "Search for:", q, function(query) { - if (query && !state.query) cm.operation(function() { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - findNext(cm, rev); - }); - }); - } - } - - function findNext(cm, rev, callback) {cm.operation(function() { - var state = getSearchState(cm); - var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); - if (!cursor.find(rev)) { - cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); - if (!cursor.find(rev)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); - state.posFrom = cursor.from(); state.posTo = cursor.to(); - if (callback) callback(cursor.from(), cursor.to()) - });} - - function clearSearch(cm) {cm.operation(function() { - var state = getSearchState(cm); - state.lastQuery = state.query; - if (!state.query) return; - state.query = state.queryText = null; - cm.removeOverlay(state.overlay); - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - });} - - var replaceQueryDialog = - ' (Use /re/ syntax for regexp search)'; - var replacementQueryDialog = 'With: '; - var doReplaceConfirm = 'Replace? '; - - function replaceAll(cm, query, text) { - cm.operation(function() { - for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { - if (typeof query != "string") { - var match = cm.getRange(cursor.from(), cursor.to()).match(query); - cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - } else cursor.replace(text); - } - }); - } - - function replace(cm, all) { - if (cm.getOption("readOnly")) return; - var query = cm.getSelection() || getSearchState(cm).lastQuery; - var dialogText = '' + (all ? 'Replace all:' : 'Replace:') + ''; - dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) { - if (!query) return; - query = parseQuery(query); - dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { - text = parseString(text) - if (all) { - replaceAll(cm, query, text) - } else { - clearSearch(cm); - var cursor = getSearchCursor(cm, query, cm.getCursor("from")); - var advance = function() { - var start = cursor.from(), match; - if (!(match = cursor.findNext())) { - cursor = getSearchCursor(cm, query); - if (!(match = cursor.findNext()) || - (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); - confirmDialog(cm, doReplaceConfirm, "Replace?", - [function() {doReplace(match);}, advance, - function() {replaceAll(cm, query, text)}]); - }; - var doReplace = function(match) { - cursor.replace(typeof query == "string" ? text : - text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - advance(); - }; - advance(); - } - }); - }); - } - - CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; - CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; - CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; - CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; - CodeMirror.commands.findNext = doSearch; - CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; - CodeMirror.commands.clearSearch = clearSearch; - CodeMirror.commands.replace = replace; - CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; -}); diff --git a/js/DevHelper/Lib/CodeMirror/addon/search/searchcursor.js b/js/DevHelper/Lib/CodeMirror/addon/search/searchcursor.js deleted file mode 100644 index eccd81a..0000000 --- a/js/DevHelper/Lib/CodeMirror/addon/search/searchcursor.js +++ /dev/null @@ -1,289 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")) - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod) - else // Plain browser env - mod(CodeMirror) -})(function(CodeMirror) { - "use strict" - var Pos = CodeMirror.Pos - - function regexpFlags(regexp) { - var flags = regexp.flags - return flags != null ? flags : (regexp.ignoreCase ? "i" : "") - + (regexp.global ? "g" : "") - + (regexp.multiline ? "m" : "") - } - - function ensureGlobal(regexp) { - return regexp.global ? regexp : new RegExp(regexp.source, regexpFlags(regexp) + "g") - } - - function maybeMultiline(regexp) { - return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) - } - - function searchRegexpForward(doc, regexp, start) { - regexp = ensureGlobal(regexp) - for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { - regexp.lastIndex = ch - var string = doc.getLine(line), match = regexp.exec(string) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpForwardMultiline(doc, regexp, start) { - if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) - - regexp = ensureGlobal(regexp) - var string, chunk = 1 - for (var line = start.line, last = doc.lastLine(); line <= last;) { - // This grows the search buffer in exponentially-sized chunks - // between matches, so that nearby matches are fast and don't - // require concatenating the whole document (in case we're - // searching for something that has tons of matches), but at the - // same time, the amount of retries is limited. - for (var i = 0; i < chunk; i++) { - var curLine = doc.getLine(line++) - string = string == null ? curLine : string + "\n" + curLine - } - chunk = chunk * 2 - regexp.lastIndex = start.ch - var match = regexp.exec(string) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - function lastMatchIn(string, regexp) { - var cutOff = 0, match - for (;;) { - regexp.lastIndex = cutOff - var newMatch = regexp.exec(string) - if (!newMatch) return match - match = newMatch - cutOff = match.index + (match[0].length || 1) - if (cutOff == string.length) return match - } - } - - function searchRegexpBackward(doc, regexp, start) { - regexp = ensureGlobal(regexp) - for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { - var string = doc.getLine(line) - if (ch > -1) string = string.slice(0, ch) - var match = lastMatchIn(string, regexp) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpBackwardMultiline(doc, regexp, start) { - regexp = ensureGlobal(regexp) - var string, chunk = 1 - for (var line = start.line, first = doc.firstLine(); line >= first;) { - for (var i = 0; i < chunk; i++) { - var curLine = doc.getLine(line--) - string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string - } - chunk *= 2 - - var match = lastMatchIn(string, regexp) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = line + before.length, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - var doFold, noFold - if (String.prototype.normalize) { - doFold = function(str) { return str.normalize("NFD").toLowerCase() } - noFold = function(str) { return str.normalize("NFD") } - } else { - doFold = function(str) { return str.toLowerCase() } - noFold = function(str) { return str } - } - - // Maps a position in a case-folded line back to a position in the original line - // (compensating for codepoints increasing in number during folding) - function adjustPos(orig, folded, pos, foldFunc) { - if (orig.length == folded.length) return pos - for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { - if (min == max) return min - var mid = (min + max) >> 1 - var len = foldFunc(orig.slice(0, mid)).length - if (len == pos) return mid - else if (len > pos) max = mid - else min = mid + 1 - } - } - - function searchStringForward(doc, query, start, caseFold) { - // Empty string would match anything and never progress, so we - // define it to match nothing instead. - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { - var orig = doc.getLine(line).slice(ch), string = fold(orig) - if (lines.length == 1) { - var found = string.indexOf(lines[0]) - if (found == -1) continue search - var start = adjustPos(orig, string, found, fold) + ch - return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} - } else { - var cutFrom = string.length - lines[0].length - if (string.slice(cutFrom) != lines[0]) continue search - for (var i = 1; i < lines.length - 1; i++) - if (fold(doc.getLine(line + i)) != lines[i]) continue search - var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] - if (end.slice(0, lastLine.length) != lastLine) continue search - return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), - to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} - } - } - } - - function searchStringBackward(doc, query, start, caseFold) { - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { - var orig = doc.getLine(line) - if (ch > -1) orig = orig.slice(0, ch) - var string = fold(orig) - if (lines.length == 1) { - var found = string.lastIndexOf(lines[0]) - if (found == -1) continue search - return {from: Pos(line, adjustPos(orig, string, found, fold)), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} - } else { - var lastLine = lines[lines.length - 1] - if (string.slice(0, lastLine.length) != lastLine) continue search - for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) - if (fold(doc.getLine(start + i)) != lines[i]) continue search - var top = doc.getLine(line + 1 - lines.length), topString = fold(top) - if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search - return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), - to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} - } - } - } - - function SearchCursor(doc, query, pos, options) { - this.atOccurrence = false - this.doc = doc - pos = pos ? doc.clipPos(pos) : Pos(0, 0) - this.pos = {from: pos, to: pos} - - var caseFold - if (typeof options == "object") { - caseFold = options.caseFold - } else { // Backwards compat for when caseFold was the 4th argument - caseFold = options - options = null - } - - if (typeof query == "string") { - if (caseFold == null) caseFold = false - this.matches = function(reverse, pos) { - return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) - } - } else { - query = ensureGlobal(query) - if (!options || options.multiline !== false) - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) - } - else - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) - } - } - } - - SearchCursor.prototype = { - findNext: function() {return this.find(false)}, - findPrevious: function() {return this.find(true)}, - - find: function(reverse) { - var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) - - // Implements weird auto-growing behavior on null-matches for - // backwards-compatiblity with the vim code (unfortunately) - while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { - if (reverse) { - if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) - else if (result.from.line == this.doc.firstLine()) result = null - else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) - } else { - if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) - else if (result.to.line == this.doc.lastLine()) result = null - else result = this.matches(reverse, Pos(result.to.line + 1, 0)) - } - } - - if (result) { - this.pos = result - this.atOccurrence = true - return this.pos.match || true - } else { - var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) - this.pos = {from: end, to: end} - return this.atOccurrence = false - } - }, - - from: function() {if (this.atOccurrence) return this.pos.from}, - to: function() {if (this.atOccurrence) return this.pos.to}, - - replace: function(newText, origin) { - if (!this.atOccurrence) return - var lines = CodeMirror.splitLines(newText) - this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) - this.pos.to = Pos(this.pos.from.line + lines.length - 1, - lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) - } - } - - CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this.doc, query, pos, caseFold) - }) - CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this, query, pos, caseFold) - }) - - CodeMirror.defineExtension("selectMatches", function(query, caseFold) { - var ranges = [] - var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) - while (cur.findNext()) { - if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break - ranges.push({anchor: cur.from(), head: cur.to()}) - } - if (ranges.length) - this.setSelections(ranges, 0) - }) -}); diff --git a/js/DevHelper/Lib/CodeMirror/lib/codemirror.css b/js/DevHelper/Lib/CodeMirror/lib/codemirror.css deleted file mode 100644 index b008351..0000000 --- a/js/DevHelper/Lib/CodeMirror/lib/codemirror.css +++ /dev/null @@ -1,340 +0,0 @@ -/* BASICS */ - -.CodeMirror { - /* Set height, width, borders, and global font properties here */ - font-family: monospace; - height: 300px; - color: black; -} - -/* PADDING */ - -.CodeMirror-lines { - padding: 4px 0; /* Vertical padding around content */ -} -.CodeMirror pre { - padding: 0 4px; /* Horizontal padding of content */ -} - -.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - background-color: white; /* The little square between H and V scrollbars */ -} - -/* GUTTER */ - -.CodeMirror-gutters { - border-right: 1px solid #ddd; - background-color: #f7f7f7; - white-space: nowrap; -} -.CodeMirror-linenumbers {} -.CodeMirror-linenumber { - padding: 0 3px 0 5px; - min-width: 20px; - text-align: right; - color: #999; - white-space: nowrap; -} - -.CodeMirror-guttermarker { color: black; } -.CodeMirror-guttermarker-subtle { color: #999; } - -/* CURSOR */ - -.CodeMirror-cursor { - border-left: 1px solid black; - border-right: none; - width: 0; -} -/* Shown when moving in bi-directional text */ -.CodeMirror div.CodeMirror-secondarycursor { - border-left: 1px solid silver; -} -.cm-fat-cursor .CodeMirror-cursor { - width: auto; - border: 0 !important; - background: #7e7; -} -.cm-fat-cursor div.CodeMirror-cursors { - z-index: 1; -} - -.cm-animate-fat-cursor { - width: auto; - border: 0; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; - background-color: #7e7; -} -@-moz-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@-webkit-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} - -/* Can style cursor different in overwrite (non-insert) mode */ -.CodeMirror-overwrite .CodeMirror-cursor {} - -.cm-tab { display: inline-block; text-decoration: inherit; } - -.CodeMirror-rulers { - position: absolute; - left: 0; right: 0; top: -50px; bottom: -20px; - overflow: hidden; -} -.CodeMirror-ruler { - border-left: 1px solid #ccc; - top: 0; bottom: 0; - position: absolute; -} - -/* DEFAULT THEME */ - -.cm-s-default .cm-header {color: blue;} -.cm-s-default .cm-quote {color: #090;} -.cm-negative {color: #d44;} -.cm-positive {color: #292;} -.cm-header, .cm-strong {font-weight: bold;} -.cm-em {font-style: italic;} -.cm-link {text-decoration: underline;} -.cm-strikethrough {text-decoration: line-through;} - -.cm-s-default .cm-keyword {color: #708;} -.cm-s-default .cm-atom {color: #219;} -.cm-s-default .cm-number {color: #164;} -.cm-s-default .cm-def {color: #00f;} -.cm-s-default .cm-variable, -.cm-s-default .cm-punctuation, -.cm-s-default .cm-property, -.cm-s-default .cm-operator {} -.cm-s-default .cm-variable-2 {color: #05a;} -.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} -.cm-s-default .cm-comment {color: #a50;} -.cm-s-default .cm-string {color: #a11;} -.cm-s-default .cm-string-2 {color: #f50;} -.cm-s-default .cm-meta {color: #555;} -.cm-s-default .cm-qualifier {color: #555;} -.cm-s-default .cm-builtin {color: #30a;} -.cm-s-default .cm-bracket {color: #997;} -.cm-s-default .cm-tag {color: #170;} -.cm-s-default .cm-attribute {color: #00c;} -.cm-s-default .cm-hr {color: #999;} -.cm-s-default .cm-link {color: #00c;} - -.cm-s-default .cm-error {color: #f00;} -.cm-invalidchar {color: #f00;} - -.CodeMirror-composing { border-bottom: 2px solid; } - -/* Default styles for common addons */ - -div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} -.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } -.CodeMirror-activeline-background {background: #e8f2ff;} - -/* STOP */ - -/* The rest of this file contains styles related to the mechanics of - the editor. You probably shouldn't touch them. */ - -.CodeMirror { - position: relative; - overflow: hidden; - background: white; -} - -.CodeMirror-scroll { - overflow: scroll !important; /* Things will break if this is overridden */ - /* 30px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror */ - margin-bottom: -30px; margin-right: -30px; - padding-bottom: 30px; - height: 100%; - outline: none; /* Prevent dragging from highlighting the element */ - position: relative; -} -.CodeMirror-sizer { - position: relative; - border-right: 30px solid transparent; -} - -/* The fake, visible scrollbars. Used to force redraw during scrolling - before actual scrolling happens, thus preventing shaking and - flickering artifacts. */ -.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - position: absolute; - z-index: 6; - display: none; -} -.CodeMirror-vscrollbar { - right: 0; top: 0; - overflow-x: hidden; - overflow-y: scroll; -} -.CodeMirror-hscrollbar { - bottom: 0; left: 0; - overflow-y: hidden; - overflow-x: scroll; -} -.CodeMirror-scrollbar-filler { - right: 0; bottom: 0; -} -.CodeMirror-gutter-filler { - left: 0; bottom: 0; -} - -.CodeMirror-gutters { - position: absolute; left: 0; top: 0; - min-height: 100%; - z-index: 3; -} -.CodeMirror-gutter { - white-space: normal; - height: 100%; - display: inline-block; - vertical-align: top; - margin-bottom: -30px; -} -.CodeMirror-gutter-wrapper { - position: absolute; - z-index: 4; - background: none !important; - border: none !important; -} -.CodeMirror-gutter-background { - position: absolute; - top: 0; bottom: 0; - z-index: 4; -} -.CodeMirror-gutter-elt { - position: absolute; - cursor: default; - z-index: 4; -} -.CodeMirror-gutter-wrapper ::selection { background-color: transparent } -.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } - -.CodeMirror-lines { - cursor: text; - min-height: 1px; /* prevents collapsing before first draw */ -} -.CodeMirror pre { - /* Reset some styles that the rest of the page might have set */ - -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; - border-width: 0; - background: transparent; - font-family: inherit; - font-size: inherit; - margin: 0; - white-space: pre; - word-wrap: normal; - line-height: inherit; - color: inherit; - z-index: 2; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; - -webkit-font-variant-ligatures: contextual; - font-variant-ligatures: contextual; -} -.CodeMirror-wrap pre { - word-wrap: break-word; - white-space: pre-wrap; - word-break: normal; -} - -.CodeMirror-linebackground { - position: absolute; - left: 0; right: 0; top: 0; bottom: 0; - z-index: 0; -} - -.CodeMirror-linewidget { - position: relative; - z-index: 2; - overflow: auto; -} - -.CodeMirror-widget {} - -.CodeMirror-rtl pre { direction: rtl; } - -.CodeMirror-code { - outline: none; -} - -/* Force content-box sizing for the elements where we expect it */ -.CodeMirror-scroll, -.CodeMirror-sizer, -.CodeMirror-gutter, -.CodeMirror-gutters, -.CodeMirror-linenumber { - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.CodeMirror-measure { - position: absolute; - width: 100%; - height: 0; - overflow: hidden; - visibility: hidden; -} - -.CodeMirror-cursor { - position: absolute; - pointer-events: none; -} -.CodeMirror-measure pre { position: static; } - -div.CodeMirror-cursors { - visibility: hidden; - position: relative; - z-index: 3; -} -div.CodeMirror-dragcursors { - visibility: visible; -} - -.CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } -.CodeMirror-crosshair { cursor: crosshair; } -.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } -.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } - -.cm-searching { - background: #ffa; - background: rgba(255, 255, 0, .4); -} - -/* Used to force a border model for a node */ -.cm-force-border { padding-right: .1px; } - -@media print { - /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursors { - visibility: hidden; - } -} - -/* See issue #2901 */ -.cm-tab-wrap-hack:after { content: ''; } - -/* Help users use markselection to safely style text background */ -span.CodeMirror-selectedtext { background: none; } diff --git a/js/DevHelper/Lib/CodeMirror/lib/codemirror.js b/js/DevHelper/Lib/CodeMirror/lib/codemirror.js deleted file mode 100644 index aba145d..0000000 --- a/js/DevHelper/Lib/CodeMirror/lib/codemirror.js +++ /dev/null @@ -1,9479 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// This is CodeMirror (http://codemirror.net), a code editor -// implemented in JavaScript on top of the browser's DOM. -// -// You can find some technical background for some of the code below -// at http://marijnhaverbeke.nl/blog/#cm-internals . - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.CodeMirror = factory()); -}(this, (function () { 'use strict'; - -// Kludges for bugs and behavior differences that can't be feature -// detected are enabled based on userAgent etc sniffing. -var userAgent = navigator.userAgent -var platform = navigator.platform - -var gecko = /gecko\/\d/i.test(userAgent) -var ie_upto10 = /MSIE \d/.test(userAgent) -var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent) -var edge = /Edge\/(\d+)/.exec(userAgent) -var ie = ie_upto10 || ie_11up || edge -var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]) -var webkit = !edge && /WebKit\//.test(userAgent) -var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent) -var chrome = !edge && /Chrome\//.test(userAgent) -var presto = /Opera\//.test(userAgent) -var safari = /Apple Computer/.test(navigator.vendor) -var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent) -var phantom = /PhantomJS/.test(userAgent) - -var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent) -var android = /Android/.test(userAgent) -// This is woefully incomplete. Suggestions for alternative methods welcome. -var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent) -var mac = ios || /Mac/.test(platform) -var chromeOS = /\bCrOS\b/.test(userAgent) -var windows = /win/i.test(platform) - -var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/) -if (presto_version) { presto_version = Number(presto_version[1]) } -if (presto_version && presto_version >= 15) { presto = false; webkit = true } -// Some browsers use the wrong event properties to signal cmd/ctrl on OS X -var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)) -var captureRightClick = gecko || (ie && ie_version >= 9) - -function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } - -var rmClass = function(node, cls) { - var current = node.className - var match = classTest(cls).exec(current) - if (match) { - var after = current.slice(match.index + match[0].length) - node.className = current.slice(0, match.index) + (after ? match[1] + after : "") - } -} - -function removeChildren(e) { - for (var count = e.childNodes.length; count > 0; --count) - { e.removeChild(e.firstChild) } - return e -} - -function removeChildrenAndAdd(parent, e) { - return removeChildren(parent).appendChild(e) -} - -function elt(tag, content, className, style) { - var e = document.createElement(tag) - if (className) { e.className = className } - if (style) { e.style.cssText = style } - if (typeof content == "string") { e.appendChild(document.createTextNode(content)) } - else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]) } } - return e -} -// wrapper for elt, which removes the elt from the accessibility tree -function eltP(tag, content, className, style) { - var e = elt(tag, content, className, style) - e.setAttribute("role", "presentation") - return e -} - -var range -if (document.createRange) { range = function(node, start, end, endNode) { - var r = document.createRange() - r.setEnd(endNode || node, end) - r.setStart(node, start) - return r -} } -else { range = function(node, start, end) { - var r = document.body.createTextRange() - try { r.moveToElementText(node.parentNode) } - catch(e) { return r } - r.collapse(true) - r.moveEnd("character", end) - r.moveStart("character", start) - return r -} } - -function contains(parent, child) { - if (child.nodeType == 3) // Android browser always returns false when child is a textnode - { child = child.parentNode } - if (parent.contains) - { return parent.contains(child) } - do { - if (child.nodeType == 11) { child = child.host } - if (child == parent) { return true } - } while (child = child.parentNode) -} - -function activeElt() { - // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. - // IE < 10 will throw when accessed while the page is loading or in an iframe. - // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. - var activeElement - try { - activeElement = document.activeElement - } catch(e) { - activeElement = document.body || null - } - while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) - { activeElement = activeElement.shadowRoot.activeElement } - return activeElement -} - -function addClass(node, cls) { - var current = node.className - if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls } -} -function joinClasses(a, b) { - var as = a.split(" ") - for (var i = 0; i < as.length; i++) - { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i] } } - return b -} - -var selectInput = function(node) { node.select() } -if (ios) // Mobile Safari apparently has a bug where select() is broken. - { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length } } -else if (ie) // Suppress mysterious IE10 errors - { selectInput = function(node) { try { node.select() } catch(_e) {} } } - -function bind(f) { - var args = Array.prototype.slice.call(arguments, 1) - return function(){return f.apply(null, args)} -} - -function copyObj(obj, target, overwrite) { - if (!target) { target = {} } - for (var prop in obj) - { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) - { target[prop] = obj[prop] } } - return target -} - -// Counts the column offset in a string, taking tabs into account. -// Used mostly to find indentation. -function countColumn(string, end, tabSize, startIndex, startValue) { - if (end == null) { - end = string.search(/[^\s\u00a0]/) - if (end == -1) { end = string.length } - } - for (var i = startIndex || 0, n = startValue || 0;;) { - var nextTab = string.indexOf("\t", i) - if (nextTab < 0 || nextTab >= end) - { return n + (end - i) } - n += nextTab - i - n += tabSize - (n % tabSize) - i = nextTab + 1 - } -} - -var Delayed = function() {this.id = null}; -Delayed.prototype.set = function (ms, f) { - clearTimeout(this.id) - this.id = setTimeout(f, ms) -}; - -function indexOf(array, elt) { - for (var i = 0; i < array.length; ++i) - { if (array[i] == elt) { return i } } - return -1 -} - -// Number of pixels added to scroller and sizer to hide scrollbar -var scrollerGap = 30 - -// Returned or thrown by various protocols to signal 'I'm not -// handling this'. -var Pass = {toString: function(){return "CodeMirror.Pass"}} - -// Reused option objects for setSelection & friends -var sel_dontScroll = {scroll: false}; -var sel_mouse = {origin: "*mouse"}; -var sel_move = {origin: "+move"}; -// The inverse of countColumn -- find the offset that corresponds to -// a particular column. -function findColumn(string, goal, tabSize) { - for (var pos = 0, col = 0;;) { - var nextTab = string.indexOf("\t", pos) - if (nextTab == -1) { nextTab = string.length } - var skipped = nextTab - pos - if (nextTab == string.length || col + skipped >= goal) - { return pos + Math.min(skipped, goal - col) } - col += nextTab - pos - col += tabSize - (col % tabSize) - pos = nextTab + 1 - if (col >= goal) { return pos } - } -} - -var spaceStrs = [""] -function spaceStr(n) { - while (spaceStrs.length <= n) - { spaceStrs.push(lst(spaceStrs) + " ") } - return spaceStrs[n] -} - -function lst(arr) { return arr[arr.length-1] } - -function map(array, f) { - var out = [] - for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i) } - return out -} - -function insertSorted(array, value, score) { - var pos = 0, priority = score(value) - while (pos < array.length && score(array[pos]) <= priority) { pos++ } - array.splice(pos, 0, value) -} - -function nothing() {} - -function createObj(base, props) { - var inst - if (Object.create) { - inst = Object.create(base) - } else { - nothing.prototype = base - inst = new nothing() - } - if (props) { copyObj(props, inst) } - return inst -} - -var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/ -function isWordCharBasic(ch) { - return /\w/.test(ch) || ch > "\x80" && - (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) -} -function isWordChar(ch, helper) { - if (!helper) { return isWordCharBasic(ch) } - if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } - return helper.test(ch) -} - -function isEmpty(obj) { - for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } - return true -} - -// Extending unicode characters. A series of a non-extending char + -// any number of extending chars is treated as a single unit as far -// as editing and measuring is concerned. This is not fully correct, -// since some scripts/fonts/browsers also treat other configurations -// of code points as a group. -var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/ -function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } - -// Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. -function skipExtendingChars(str, pos, dir) { - while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir } - return pos -} - -// Returns the value from the range [`from`; `to`] that satisfies -// `pred` and is closest to `from`. Assumes that at least `to` satisfies `pred`. -function findFirst(pred, from, to) { - for (;;) { - if (Math.abs(from - to) <= 1) { return pred(from) ? from : to } - var mid = Math.floor((from + to) / 2) - if (pred(mid)) { to = mid } - else { from = mid } - } -} - -// The display handles the DOM integration, both for input reading -// and content drawing. It holds references to DOM nodes and -// display-related state. - -function Display(place, doc, input) { - var d = this - this.input = input - - // Covers bottom-right square when both scrollbars are present. - d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler") - d.scrollbarFiller.setAttribute("cm-not-content", "true") - // Covers bottom of gutter when coverGutterNextToScrollbar is on - // and h scrollbar is present. - d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler") - d.gutterFiller.setAttribute("cm-not-content", "true") - // Will contain the actual code, positioned to cover the viewport. - d.lineDiv = eltP("div", null, "CodeMirror-code") - // Elements are added to these to represent selection and cursors. - d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1") - d.cursorDiv = elt("div", null, "CodeMirror-cursors") - // A visibility: hidden element used to find the size of things. - d.measure = elt("div", null, "CodeMirror-measure") - // When lines outside of the viewport are measured, they are drawn in this. - d.lineMeasure = elt("div", null, "CodeMirror-measure") - // Wraps everything that needs to exist inside the vertically-padded coordinate system - d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], - null, "position: relative; outline: none") - var lines = eltP("div", [d.lineSpace], "CodeMirror-lines") - // Moved around its parent to cover visible view. - d.mover = elt("div", [lines], null, "position: relative") - // Set to the height of the document, allowing scrolling. - d.sizer = elt("div", [d.mover], "CodeMirror-sizer") - d.sizerWidth = null - // Behavior of elts with overflow: auto and padding is - // inconsistent across browsers. This is used to ensure the - // scrollable area is big enough. - d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;") - // Will contain the gutters, if any. - d.gutters = elt("div", null, "CodeMirror-gutters") - d.lineGutter = null - // Actual scrollable element. - d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll") - d.scroller.setAttribute("tabIndex", "-1") - // The element in which the editor lives. - d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror") - - // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) - if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0 } - if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true } - - if (place) { - if (place.appendChild) { place.appendChild(d.wrapper) } - else { place(d.wrapper) } - } - - // Current rendered range (may be bigger than the view window). - d.viewFrom = d.viewTo = doc.first - d.reportedViewFrom = d.reportedViewTo = doc.first - // Information about the rendered lines. - d.view = [] - d.renderedView = null - // Holds info about a single rendered line when it was rendered - // for measurement, while not in view. - d.externalMeasured = null - // Empty space (in pixels) above the view - d.viewOffset = 0 - d.lastWrapHeight = d.lastWrapWidth = 0 - d.updateLineNumbers = null - - d.nativeBarWidth = d.barHeight = d.barWidth = 0 - d.scrollbarsClipped = false - - // Used to only resize the line number gutter when necessary (when - // the amount of lines crosses a boundary that makes its width change) - d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null - // Set to true when a non-horizontal-scrolling line widget is - // added. As an optimization, line widget aligning is skipped when - // this is false. - d.alignWidgets = false - - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null - - // Tracks the maximum line length so that the horizontal scrollbar - // can be kept static when scrolling. - d.maxLine = null - d.maxLineLength = 0 - d.maxLineChanged = false - - // Used for measuring wheel scrolling granularity - d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null - - // True when shift is held down. - d.shift = false - - // Used to track whether anything happened since the context menu - // was opened. - d.selForContextMenu = null - - d.activeTouch = null - - input.init(d) -} - -// Find the line object corresponding to the given line number. -function getLine(doc, n) { - n -= doc.first - if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } - var chunk = doc - while (!chunk.lines) { - for (var i = 0;; ++i) { - var child = chunk.children[i], sz = child.chunkSize() - if (n < sz) { chunk = child; break } - n -= sz - } - } - return chunk.lines[n] -} - -// Get the part of a document between two positions, as an array of -// strings. -function getBetween(doc, start, end) { - var out = [], n = start.line - doc.iter(start.line, end.line + 1, function (line) { - var text = line.text - if (n == end.line) { text = text.slice(0, end.ch) } - if (n == start.line) { text = text.slice(start.ch) } - out.push(text) - ++n - }) - return out -} -// Get the lines between from and to, as array of strings. -function getLines(doc, from, to) { - var out = [] - doc.iter(from, to, function (line) { out.push(line.text) }) // iter aborts when callback returns truthy value - return out -} - -// Update the height of a line, propagating the height change -// upwards to parent nodes. -function updateLineHeight(line, height) { - var diff = height - line.height - if (diff) { for (var n = line; n; n = n.parent) { n.height += diff } } -} - -// Given a line object, find its line number by walking up through -// its parent links. -function lineNo(line) { - if (line.parent == null) { return null } - var cur = line.parent, no = indexOf(cur.lines, line) - for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { - for (var i = 0;; ++i) { - if (chunk.children[i] == cur) { break } - no += chunk.children[i].chunkSize() - } - } - return no + cur.first -} - -// Find the line at the given vertical position, using the height -// information in the document tree. -function lineAtHeight(chunk, h) { - var n = chunk.first - outer: do { - for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { - var child = chunk.children[i$1], ch = child.height - if (h < ch) { chunk = child; continue outer } - h -= ch - n += child.chunkSize() - } - return n - } while (!chunk.lines) - var i = 0 - for (; i < chunk.lines.length; ++i) { - var line = chunk.lines[i], lh = line.height - if (h < lh) { break } - h -= lh - } - return n + i -} - -function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} - -function lineNumberFor(options, i) { - return String(options.lineNumberFormatter(i + options.firstLineNumber)) -} - -// A Pos instance represents a position within the text. -function Pos(line, ch, sticky) { - if ( sticky === void 0 ) sticky = null; - - if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } - this.line = line - this.ch = ch - this.sticky = sticky -} - -// Compare two positions, return 0 if they are the same, a negative -// number when a is less, and a positive number otherwise. -function cmp(a, b) { return a.line - b.line || a.ch - b.ch } - -function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } - -function copyPos(x) {return Pos(x.line, x.ch)} -function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } -function minPos(a, b) { return cmp(a, b) < 0 ? a : b } - -// Most of the external API clips given positions to make sure they -// actually exist within the document. -function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} -function clipPos(doc, pos) { - if (pos.line < doc.first) { return Pos(doc.first, 0) } - var last = doc.first + doc.size - 1 - if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } - return clipToLen(pos, getLine(doc, pos.line).text.length) -} -function clipToLen(pos, linelen) { - var ch = pos.ch - if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } - else if (ch < 0) { return Pos(pos.line, 0) } - else { return pos } -} -function clipPosArray(doc, array) { - var out = [] - for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]) } - return out -} - -// Optimize some code when these features are not used. -var sawReadOnlySpans = false; -var sawCollapsedSpans = false; -function seeReadOnlySpans() { - sawReadOnlySpans = true -} - -function seeCollapsedSpans() { - sawCollapsedSpans = true -} - -// TEXTMARKER SPANS - -function MarkedSpan(marker, from, to) { - this.marker = marker - this.from = from; this.to = to -} - -// Search an array of spans for a span matching the given marker. -function getMarkedSpanFor(spans, marker) { - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i] - if (span.marker == marker) { return span } - } } -} -// Remove a span from an array, returning undefined if no spans are -// left (we don't store arrays for lines without spans). -function removeMarkedSpan(spans, span) { - var r - for (var i = 0; i < spans.length; ++i) - { if (spans[i] != span) { (r || (r = [])).push(spans[i]) } } - return r -} -// Add a span to a line. -function addMarkedSpan(line, span) { - line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span] - span.marker.attachLine(line) -} - -// Used for the algorithm that adjusts markers for a change in the -// document. These functions cut an array of spans at a given -// character position, returning an array of remaining chunks (or -// undefined if nothing remains). -function markedSpansBefore(old, startCh, isInsert) { - var nw - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh) - if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)) - } - } } - return nw -} -function markedSpansAfter(old, endCh, isInsert) { - var nw - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh) - if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, - span.to == null ? null : span.to - endCh)) - } - } } - return nw -} - -// Given a change object, compute the new set of marker spans that -// cover the line in which the change took place. Removes spans -// entirely within the change, reconnects spans belonging to the -// same marker that appear on both sides of the change, and cuts off -// spans partially within the change. Returns an array of span -// arrays with one element for each line in (after) the change. -function stretchSpansOverChange(doc, change) { - if (change.full) { return null } - var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans - var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans - if (!oldFirst && !oldLast) { return null } - - var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0 - // Get the spans that 'stick out' on both sides - var first = markedSpansBefore(oldFirst, startCh, isInsert) - var last = markedSpansAfter(oldLast, endCh, isInsert) - - // Next, merge those two ends - var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0) - if (first) { - // Fix up .to properties of first - for (var i = 0; i < first.length; ++i) { - var span = first[i] - if (span.to == null) { - var found = getMarkedSpanFor(last, span.marker) - if (!found) { span.to = startCh } - else if (sameLine) { span.to = found.to == null ? null : found.to + offset } - } - } - } - if (last) { - // Fix up .from in last (or move them into first in case of sameLine) - for (var i$1 = 0; i$1 < last.length; ++i$1) { - var span$1 = last[i$1] - if (span$1.to != null) { span$1.to += offset } - if (span$1.from == null) { - var found$1 = getMarkedSpanFor(first, span$1.marker) - if (!found$1) { - span$1.from = offset - if (sameLine) { (first || (first = [])).push(span$1) } - } - } else { - span$1.from += offset - if (sameLine) { (first || (first = [])).push(span$1) } - } - } - } - // Make sure we didn't create any zero-length spans - if (first) { first = clearEmptySpans(first) } - if (last && last != first) { last = clearEmptySpans(last) } - - var newMarkers = [first] - if (!sameLine) { - // Fill gap with whole-line-spans - var gap = change.text.length - 2, gapMarkers - if (gap > 0 && first) - { for (var i$2 = 0; i$2 < first.length; ++i$2) - { if (first[i$2].to == null) - { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)) } } } - for (var i$3 = 0; i$3 < gap; ++i$3) - { newMarkers.push(gapMarkers) } - newMarkers.push(last) - } - return newMarkers -} - -// Remove spans that are empty and don't have a clearWhenEmpty -// option of false. -function clearEmptySpans(spans) { - for (var i = 0; i < spans.length; ++i) { - var span = spans[i] - if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) - { spans.splice(i--, 1) } - } - if (!spans.length) { return null } - return spans -} - -// Used to 'clip' out readOnly ranges when making a change. -function removeReadOnlyRanges(doc, from, to) { - var markers = null - doc.iter(from.line, to.line + 1, function (line) { - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var mark = line.markedSpans[i].marker - if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) - { (markers || (markers = [])).push(mark) } - } } - }) - if (!markers) { return null } - var parts = [{from: from, to: to}] - for (var i = 0; i < markers.length; ++i) { - var mk = markers[i], m = mk.find(0) - for (var j = 0; j < parts.length; ++j) { - var p = parts[j] - if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } - var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to) - if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) - { newParts.push({from: p.from, to: m.from}) } - if (dto > 0 || !mk.inclusiveRight && !dto) - { newParts.push({from: m.to, to: p.to}) } - parts.splice.apply(parts, newParts) - j += newParts.length - 3 - } - } - return parts -} - -// Connect or disconnect spans from a line. -function detachMarkedSpans(line) { - var spans = line.markedSpans - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.detachLine(line) } - line.markedSpans = null -} -function attachMarkedSpans(line, spans) { - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.attachLine(line) } - line.markedSpans = spans -} - -// Helpers used when computing which overlapping collapsed span -// counts as the larger one. -function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } -function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } - -// Returns a number indicating which of two overlapping collapsed -// spans is larger (and thus includes the other). Falls back to -// comparing ids when the spans cover exactly the same range. -function compareCollapsedMarkers(a, b) { - var lenDiff = a.lines.length - b.lines.length - if (lenDiff != 0) { return lenDiff } - var aPos = a.find(), bPos = b.find() - var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b) - if (fromCmp) { return -fromCmp } - var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b) - if (toCmp) { return toCmp } - return b.id - a.id -} - -// Find out whether a line ends or starts in a collapsed span. If -// so, return the marker for that span. -function collapsedSpanAtSide(line, start) { - var sps = sawCollapsedSpans && line.markedSpans, found - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i] - if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && - (!found || compareCollapsedMarkers(found, sp.marker) < 0)) - { found = sp.marker } - } } - return found -} -function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } -function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } - -// Test whether there exists a collapsed span that partially -// overlaps (covers the start or end, but not both) of a new span. -// Such overlap is not allowed. -function conflictingCollapsedRange(doc, lineNo, from, to, marker) { - var line = getLine(doc, lineNo) - var sps = sawCollapsedSpans && line.markedSpans - if (sps) { for (var i = 0; i < sps.length; ++i) { - var sp = sps[i] - if (!sp.marker.collapsed) { continue } - var found = sp.marker.find(0) - var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker) - var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker) - if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } - if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || - fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) - { return true } - } } -} - -// A visual line is a line as drawn on the screen. Folding, for -// example, can cause multiple logical lines to appear on the same -// visual line. This finds the start of the visual line that the -// given line is part of (usually that is the line itself). -function visualLine(line) { - var merged - while (merged = collapsedSpanAtStart(line)) - { line = merged.find(-1, true).line } - return line -} - -function visualLineEnd(line) { - var merged - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line } - return line -} - -// Returns an array of logical lines that continue the visual line -// started by the argument, or undefined if there are no such lines. -function visualLineContinued(line) { - var merged, lines - while (merged = collapsedSpanAtEnd(line)) { - line = merged.find(1, true).line - ;(lines || (lines = [])).push(line) - } - return lines -} - -// Get the line number of the start of the visual line that the -// given line number is part of. -function visualLineNo(doc, lineN) { - var line = getLine(doc, lineN), vis = visualLine(line) - if (line == vis) { return lineN } - return lineNo(vis) -} - -// Get the line number of the start of the next visual line after -// the given line. -function visualLineEndNo(doc, lineN) { - if (lineN > doc.lastLine()) { return lineN } - var line = getLine(doc, lineN), merged - if (!lineIsHidden(doc, line)) { return lineN } - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line } - return lineNo(line) + 1 -} - -// Compute whether a line is hidden. Lines count as hidden when they -// are part of a visual line that starts with another line, or when -// they are entirely covered by collapsed, non-widget span. -function lineIsHidden(doc, line) { - var sps = sawCollapsedSpans && line.markedSpans - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i] - if (!sp.marker.collapsed) { continue } - if (sp.from == null) { return true } - if (sp.marker.widgetNode) { continue } - if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) - { return true } - } } -} -function lineIsHiddenInner(doc, line, span) { - if (span.to == null) { - var end = span.marker.find(1, true) - return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) - } - if (span.marker.inclusiveRight && span.to == line.text.length) - { return true } - for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { - sp = line.markedSpans[i] - if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && - (sp.to == null || sp.to != span.from) && - (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && - lineIsHiddenInner(doc, line, sp)) { return true } - } -} - -// Find the height above the given line. -function heightAtLine(lineObj) { - lineObj = visualLine(lineObj) - - var h = 0, chunk = lineObj.parent - for (var i = 0; i < chunk.lines.length; ++i) { - var line = chunk.lines[i] - if (line == lineObj) { break } - else { h += line.height } - } - for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { - for (var i$1 = 0; i$1 < p.children.length; ++i$1) { - var cur = p.children[i$1] - if (cur == chunk) { break } - else { h += cur.height } - } - } - return h -} - -// Compute the character length of a line, taking into account -// collapsed ranges (see markText) that might hide parts, and join -// other lines onto it. -function lineLength(line) { - if (line.height == 0) { return 0 } - var len = line.text.length, merged, cur = line - while (merged = collapsedSpanAtStart(cur)) { - var found = merged.find(0, true) - cur = found.from.line - len += found.from.ch - found.to.ch - } - cur = line - while (merged = collapsedSpanAtEnd(cur)) { - var found$1 = merged.find(0, true) - len -= cur.text.length - found$1.from.ch - cur = found$1.to.line - len += cur.text.length - found$1.to.ch - } - return len -} - -// Find the longest line in the document. -function findMaxLine(cm) { - var d = cm.display, doc = cm.doc - d.maxLine = getLine(doc, doc.first) - d.maxLineLength = lineLength(d.maxLine) - d.maxLineChanged = true - doc.iter(function (line) { - var len = lineLength(line) - if (len > d.maxLineLength) { - d.maxLineLength = len - d.maxLine = line - } - }) -} - -// BIDI HELPERS - -function iterateBidiSections(order, from, to, f) { - if (!order) { return f(from, to, "ltr") } - var found = false - for (var i = 0; i < order.length; ++i) { - var part = order[i] - if (part.from < to && part.to > from || from == to && part.to == from) { - f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr") - found = true - } - } - if (!found) { f(from, to, "ltr") } -} - -var bidiOther = null -function getBidiPartAt(order, ch, sticky) { - var found - bidiOther = null - for (var i = 0; i < order.length; ++i) { - var cur = order[i] - if (cur.from < ch && cur.to > ch) { return i } - if (cur.to == ch) { - if (cur.from != cur.to && sticky == "before") { found = i } - else { bidiOther = i } - } - if (cur.from == ch) { - if (cur.from != cur.to && sticky != "before") { found = i } - else { bidiOther = i } - } - } - return found != null ? found : bidiOther -} - -// Bidirectional ordering algorithm -// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm -// that this (partially) implements. - -// One-char codes used for character types: -// L (L): Left-to-Right -// R (R): Right-to-Left -// r (AL): Right-to-Left Arabic -// 1 (EN): European Number -// + (ES): European Number Separator -// % (ET): European Number Terminator -// n (AN): Arabic Number -// , (CS): Common Number Separator -// m (NSM): Non-Spacing Mark -// b (BN): Boundary Neutral -// s (B): Paragraph Separator -// t (S): Segment Separator -// w (WS): Whitespace -// N (ON): Other Neutrals - -// Returns null if characters are ordered as they appear -// (left-to-right), or an array of sections ({from, to, level} -// objects) in the order in which they occur visually. -var bidiOrdering = (function() { - // Character types for codepoints 0 to 0xff - var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN" - // Character types for codepoints 0x600 to 0x6f9 - var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111" - function charType(code) { - if (code <= 0xf7) { return lowTypes.charAt(code) } - else if (0x590 <= code && code <= 0x5f4) { return "R" } - else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } - else if (0x6ee <= code && code <= 0x8ac) { return "r" } - else if (0x2000 <= code && code <= 0x200b) { return "w" } - else if (code == 0x200c) { return "b" } - else { return "L" } - } - - var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/ - var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/ - - function BidiSpan(level, from, to) { - this.level = level - this.from = from; this.to = to - } - - return function(str, direction) { - var outerType = direction == "ltr" ? "L" : "R" - - if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } - var len = str.length, types = [] - for (var i = 0; i < len; ++i) - { types.push(charType(str.charCodeAt(i))) } - - // W1. Examine each non-spacing mark (NSM) in the level run, and - // change the type of the NSM to the type of the previous - // character. If the NSM is at the start of the level run, it will - // get the type of sor. - for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { - var type = types[i$1] - if (type == "m") { types[i$1] = prev } - else { prev = type } - } - - // W2. Search backwards from each instance of a European number - // until the first strong type (R, L, AL, or sor) is found. If an - // AL is found, change the type of the European number to Arabic - // number. - // W3. Change all ALs to R. - for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { - var type$1 = types[i$2] - if (type$1 == "1" && cur == "r") { types[i$2] = "n" } - else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R" } } - } - - // W4. A single European separator between two European numbers - // changes to a European number. A single common separator between - // two numbers of the same type changes to that type. - for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { - var type$2 = types[i$3] - if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1" } - else if (type$2 == "," && prev$1 == types[i$3+1] && - (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1 } - prev$1 = type$2 - } - - // W5. A sequence of European terminators adjacent to European - // numbers changes to all European numbers. - // W6. Otherwise, separators and terminators change to Other - // Neutral. - for (var i$4 = 0; i$4 < len; ++i$4) { - var type$3 = types[i$4] - if (type$3 == ",") { types[i$4] = "N" } - else if (type$3 == "%") { - var end = (void 0) - for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} - var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N" - for (var j = i$4; j < end; ++j) { types[j] = replace } - i$4 = end - 1 - } - } - - // W7. Search backwards from each instance of a European number - // until the first strong type (R, L, or sor) is found. If an L is - // found, then change the type of the European number to L. - for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { - var type$4 = types[i$5] - if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L" } - else if (isStrong.test(type$4)) { cur$1 = type$4 } - } - - // N1. A sequence of neutrals takes the direction of the - // surrounding strong text if the text on both sides has the same - // direction. European and Arabic numbers act as if they were R in - // terms of their influence on neutrals. Start-of-level-run (sor) - // and end-of-level-run (eor) are used at level run boundaries. - // N2. Any remaining neutrals take the embedding direction. - for (var i$6 = 0; i$6 < len; ++i$6) { - if (isNeutral.test(types[i$6])) { - var end$1 = (void 0) - for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} - var before = (i$6 ? types[i$6-1] : outerType) == "L" - var after = (end$1 < len ? types[end$1] : outerType) == "L" - var replace$1 = before == after ? (before ? "L" : "R") : outerType - for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1 } - i$6 = end$1 - 1 - } - } - - // Here we depart from the documented algorithm, in order to avoid - // building up an actual levels array. Since there are only three - // levels (0, 1, 2) in an implementation that doesn't take - // explicit embedding into account, we can build up the order on - // the fly, without following the level-based algorithm. - var order = [], m - for (var i$7 = 0; i$7 < len;) { - if (countsAsLeft.test(types[i$7])) { - var start = i$7 - for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} - order.push(new BidiSpan(0, start, i$7)) - } else { - var pos = i$7, at = order.length - for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} - for (var j$2 = pos; j$2 < i$7;) { - if (countsAsNum.test(types[j$2])) { - if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)) } - var nstart = j$2 - for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} - order.splice(at, 0, new BidiSpan(2, nstart, j$2)) - pos = j$2 - } else { ++j$2 } - } - if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)) } - } - } - if (order[0].level == 1 && (m = str.match(/^\s+/))) { - order[0].from = m[0].length - order.unshift(new BidiSpan(0, 0, m[0].length)) - } - if (lst(order).level == 1 && (m = str.match(/\s+$/))) { - lst(order).to -= m[0].length - order.push(new BidiSpan(0, len - m[0].length, len)) - } - - return direction == "rtl" ? order.reverse() : order - } -})() - -// Get the bidi ordering for the given line (and cache it). Returns -// false for lines that are fully left-to-right, and an array of -// BidiSpan objects otherwise. -function getOrder(line, direction) { - var order = line.order - if (order == null) { order = line.order = bidiOrdering(line.text, direction) } - return order -} - -function moveCharLogically(line, ch, dir) { - var target = skipExtendingChars(line.text, ch + dir, dir) - return target < 0 || target > line.text.length ? null : target -} - -function moveLogically(line, start, dir) { - var ch = moveCharLogically(line, start.ch, dir) - return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") -} - -function endOfLine(visually, cm, lineObj, lineNo, dir) { - if (visually) { - var order = getOrder(lineObj, cm.doc.direction) - if (order) { - var part = dir < 0 ? lst(order) : order[0] - var moveInStorageOrder = (dir < 0) == (part.level == 1) - var sticky = moveInStorageOrder ? "after" : "before" - var ch - // With a wrapped rtl chunk (possibly spanning multiple bidi parts), - // it could be that the last bidi part is not on the last visual line, - // since visual lines contain content order-consecutive chunks. - // Thus, in rtl, we are looking for the first (content-order) character - // in the rtl chunk that is on the last line (that is, the same line - // as the last (content-order) character). - if (part.level > 0) { - var prep = prepareMeasureForLine(cm, lineObj) - ch = dir < 0 ? lineObj.text.length - 1 : 0 - var targetTop = measureCharPrepared(cm, prep, ch).top - ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch) - if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1) } - } else { ch = dir < 0 ? part.to : part.from } - return new Pos(lineNo, ch, sticky) - } - } - return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") -} - -function moveVisually(cm, line, start, dir) { - var bidi = getOrder(line, cm.doc.direction) - if (!bidi) { return moveLogically(line, start, dir) } - if (start.ch >= line.text.length) { - start.ch = line.text.length - start.sticky = "before" - } else if (start.ch <= 0) { - start.ch = 0 - start.sticky = "after" - } - var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos] - if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { - // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, - // nothing interesting happens. - return moveLogically(line, start, dir) - } - - var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); } - var prep - var getWrappedLineExtent = function (ch) { - if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } - prep = prep || prepareMeasureForLine(cm, line) - return wrappedLineExtentChar(cm, line, prep, ch) - } - var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch) - - if (cm.doc.direction == "rtl" || part.level == 1) { - var moveInStorageOrder = (part.level == 1) == (dir < 0) - var ch = mv(start, moveInStorageOrder ? 1 : -1) - if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { - // Case 2: We move within an rtl part or in an rtl editor on the same visual line - var sticky = moveInStorageOrder ? "before" : "after" - return new Pos(start.line, ch, sticky) - } - } - - // Case 3: Could not move within this bidi part in this visual line, so leave - // the current bidi part - - var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { - var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder - ? new Pos(start.line, mv(ch, 1), "before") - : new Pos(start.line, ch, "after"); } - - for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { - var part = bidi[partPos] - var moveInStorageOrder = (dir > 0) == (part.level != 1) - var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1) - if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } - ch = moveInStorageOrder ? part.from : mv(part.to, -1) - if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } - } - } - - // Case 3a: Look for other bidi parts on the same visual line - var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent) - if (res) { return res } - - // Case 3b: Look for other bidi parts on the next visual line - var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1) - if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { - res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)) - if (res) { return res } - } - - // Case 4: Nowhere to move - return null -} - -// EVENT HANDLING - -// Lightweight event framework. on/off also work on DOM nodes, -// registering native DOM handlers. - -var noHandlers = [] - -var on = function(emitter, type, f) { - if (emitter.addEventListener) { - emitter.addEventListener(type, f, false) - } else if (emitter.attachEvent) { - emitter.attachEvent("on" + type, f) - } else { - var map = emitter._handlers || (emitter._handlers = {}) - map[type] = (map[type] || noHandlers).concat(f) - } -} - -function getHandlers(emitter, type) { - return emitter._handlers && emitter._handlers[type] || noHandlers -} - -function off(emitter, type, f) { - if (emitter.removeEventListener) { - emitter.removeEventListener(type, f, false) - } else if (emitter.detachEvent) { - emitter.detachEvent("on" + type, f) - } else { - var map = emitter._handlers, arr = map && map[type] - if (arr) { - var index = indexOf(arr, f) - if (index > -1) - { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)) } - } - } -} - -function signal(emitter, type /*, values...*/) { - var handlers = getHandlers(emitter, type) - if (!handlers.length) { return } - var args = Array.prototype.slice.call(arguments, 2) - for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args) } -} - -// The DOM events that CodeMirror handles can be overridden by -// registering a (non-DOM) handler on the editor for the event name, -// and preventDefault-ing the event in that handler. -function signalDOMEvent(cm, e, override) { - if (typeof e == "string") - { e = {type: e, preventDefault: function() { this.defaultPrevented = true }} } - signal(cm, override || e.type, cm, e) - return e_defaultPrevented(e) || e.codemirrorIgnore -} - -function signalCursorActivity(cm) { - var arr = cm._handlers && cm._handlers.cursorActivity - if (!arr) { return } - var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []) - for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) - { set.push(arr[i]) } } -} - -function hasHandler(emitter, type) { - return getHandlers(emitter, type).length > 0 -} - -// Add on and off methods to a constructor's prototype, to make -// registering events on such objects more convenient. -function eventMixin(ctor) { - ctor.prototype.on = function(type, f) {on(this, type, f)} - ctor.prototype.off = function(type, f) {off(this, type, f)} -} - -// Due to the fact that we still support jurassic IE versions, some -// compatibility wrappers are needed. - -function e_preventDefault(e) { - if (e.preventDefault) { e.preventDefault() } - else { e.returnValue = false } -} -function e_stopPropagation(e) { - if (e.stopPropagation) { e.stopPropagation() } - else { e.cancelBubble = true } -} -function e_defaultPrevented(e) { - return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false -} -function e_stop(e) {e_preventDefault(e); e_stopPropagation(e)} - -function e_target(e) {return e.target || e.srcElement} -function e_button(e) { - var b = e.which - if (b == null) { - if (e.button & 1) { b = 1 } - else if (e.button & 2) { b = 3 } - else if (e.button & 4) { b = 2 } - } - if (mac && e.ctrlKey && b == 1) { b = 3 } - return b -} - -// Detect drag-and-drop -var dragAndDrop = function() { - // There is *some* kind of drag-and-drop support in IE6-8, but I - // couldn't get it to work yet. - if (ie && ie_version < 9) { return false } - var div = elt('div') - return "draggable" in div || "dragDrop" in div -}() - -var zwspSupported -function zeroWidthElement(measure) { - if (zwspSupported == null) { - var test = elt("span", "\u200b") - removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])) - if (measure.firstChild.offsetHeight != 0) - { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8) } - } - var node = zwspSupported ? elt("span", "\u200b") : - elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px") - node.setAttribute("cm-text", "") - return node -} - -// Feature-detect IE's crummy client rect reporting for bidi text -var badBidiRects -function hasBadBidiRects(measure) { - if (badBidiRects != null) { return badBidiRects } - var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")) - var r0 = range(txt, 0, 1).getBoundingClientRect() - var r1 = range(txt, 1, 2).getBoundingClientRect() - removeChildren(measure) - if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) - return badBidiRects = (r1.right - r0.right < 3) -} - -// See if "".split is the broken IE version, if so, provide an -// alternative way to split lines. -var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { - var pos = 0, result = [], l = string.length - while (pos <= l) { - var nl = string.indexOf("\n", pos) - if (nl == -1) { nl = string.length } - var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl) - var rt = line.indexOf("\r") - if (rt != -1) { - result.push(line.slice(0, rt)) - pos += rt + 1 - } else { - result.push(line) - pos = nl + 1 - } - } - return result -} : function (string) { return string.split(/\r\n?|\n/); } - -var hasSelection = window.getSelection ? function (te) { - try { return te.selectionStart != te.selectionEnd } - catch(e) { return false } -} : function (te) { - var range - try {range = te.ownerDocument.selection.createRange()} - catch(e) {} - if (!range || range.parentElement() != te) { return false } - return range.compareEndPoints("StartToEnd", range) != 0 -} - -var hasCopyEvent = (function () { - var e = elt("div") - if ("oncopy" in e) { return true } - e.setAttribute("oncopy", "return;") - return typeof e.oncopy == "function" -})() - -var badZoomedRects = null -function hasBadZoomedRects(measure) { - if (badZoomedRects != null) { return badZoomedRects } - var node = removeChildrenAndAdd(measure, elt("span", "x")) - var normal = node.getBoundingClientRect() - var fromRange = range(node, 0, 1).getBoundingClientRect() - return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 -} - -var modes = {}; -var mimeModes = {}; -// Extra arguments are stored as the mode's dependencies, which is -// used by (legacy) mechanisms like loadmode.js to automatically -// load a mode. (Preferred mechanism is the require/define calls.) -function defineMode(name, mode) { - if (arguments.length > 2) - { mode.dependencies = Array.prototype.slice.call(arguments, 2) } - modes[name] = mode -} - -function defineMIME(mime, spec) { - mimeModes[mime] = spec -} - -// Given a MIME type, a {name, ...options} config object, or a name -// string, return a mode config object. -function resolveMode(spec) { - if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { - spec = mimeModes[spec] - } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { - var found = mimeModes[spec.name] - if (typeof found == "string") { found = {name: found} } - spec = createObj(found, spec) - spec.name = found.name - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { - return resolveMode("application/xml") - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { - return resolveMode("application/json") - } - if (typeof spec == "string") { return {name: spec} } - else { return spec || {name: "null"} } -} - -// Given a mode spec (anything that resolveMode accepts), find and -// initialize an actual mode object. -function getMode(options, spec) { - spec = resolveMode(spec) - var mfactory = modes[spec.name] - if (!mfactory) { return getMode(options, "text/plain") } - var modeObj = mfactory(options, spec) - if (modeExtensions.hasOwnProperty(spec.name)) { - var exts = modeExtensions[spec.name] - for (var prop in exts) { - if (!exts.hasOwnProperty(prop)) { continue } - if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop] } - modeObj[prop] = exts[prop] - } - } - modeObj.name = spec.name - if (spec.helperType) { modeObj.helperType = spec.helperType } - if (spec.modeProps) { for (var prop$1 in spec.modeProps) - { modeObj[prop$1] = spec.modeProps[prop$1] } } - - return modeObj -} - -// This can be used to attach properties to mode objects from -// outside the actual mode definition. -var modeExtensions = {} -function extendMode(mode, properties) { - var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}) - copyObj(properties, exts) -} - -function copyState(mode, state) { - if (state === true) { return state } - if (mode.copyState) { return mode.copyState(state) } - var nstate = {} - for (var n in state) { - var val = state[n] - if (val instanceof Array) { val = val.concat([]) } - nstate[n] = val - } - return nstate -} - -// Given a mode and a state (for that mode), find the inner mode and -// state at the position that the state refers to. -function innerMode(mode, state) { - var info - while (mode.innerMode) { - info = mode.innerMode(state) - if (!info || info.mode == mode) { break } - state = info.state - mode = info.mode - } - return info || {mode: mode, state: state} -} - -function startState(mode, a1, a2) { - return mode.startState ? mode.startState(a1, a2) : true -} - -// STRING STREAM - -// Fed to the mode parsers, provides helper functions to make -// parsers more succinct. - -var StringStream = function(string, tabSize, lineOracle) { - this.pos = this.start = 0 - this.string = string - this.tabSize = tabSize || 8 - this.lastColumnPos = this.lastColumnValue = 0 - this.lineStart = 0 - this.lineOracle = lineOracle -}; - -StringStream.prototype.eol = function () {return this.pos >= this.string.length}; -StringStream.prototype.sol = function () {return this.pos == this.lineStart}; -StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; -StringStream.prototype.next = function () { - if (this.pos < this.string.length) - { return this.string.charAt(this.pos++) } -}; -StringStream.prototype.eat = function (match) { - var ch = this.string.charAt(this.pos) - var ok - if (typeof match == "string") { ok = ch == match } - else { ok = ch && (match.test ? match.test(ch) : match(ch)) } - if (ok) {++this.pos; return ch} -}; -StringStream.prototype.eatWhile = function (match) { - var start = this.pos - while (this.eat(match)){} - return this.pos > start -}; -StringStream.prototype.eatSpace = function () { - var this$1 = this; - - var start = this.pos - while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos } - return this.pos > start -}; -StringStream.prototype.skipToEnd = function () {this.pos = this.string.length}; -StringStream.prototype.skipTo = function (ch) { - var found = this.string.indexOf(ch, this.pos) - if (found > -1) {this.pos = found; return true} -}; -StringStream.prototype.backUp = function (n) {this.pos -= n}; -StringStream.prototype.column = function () { - if (this.lastColumnPos < this.start) { - this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue) - this.lastColumnPos = this.start - } - return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) -}; -StringStream.prototype.indentation = function () { - return countColumn(this.string, null, this.tabSize) - - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) -}; -StringStream.prototype.match = function (pattern, consume, caseInsensitive) { - if (typeof pattern == "string") { - var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; } - var substr = this.string.substr(this.pos, pattern.length) - if (cased(substr) == cased(pattern)) { - if (consume !== false) { this.pos += pattern.length } - return true - } - } else { - var match = this.string.slice(this.pos).match(pattern) - if (match && match.index > 0) { return null } - if (match && consume !== false) { this.pos += match[0].length } - return match - } -}; -StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; -StringStream.prototype.hideFirstChars = function (n, inner) { - this.lineStart += n - try { return inner() } - finally { this.lineStart -= n } -}; -StringStream.prototype.lookAhead = function (n) { - var oracle = this.lineOracle - return oracle && oracle.lookAhead(n) -}; - -var SavedContext = function(state, lookAhead) { - this.state = state - this.lookAhead = lookAhead -}; - -var Context = function(doc, state, line, lookAhead) { - this.state = state - this.doc = doc - this.line = line - this.maxLookAhead = lookAhead || 0 -}; - -Context.prototype.lookAhead = function (n) { - var line = this.doc.getLine(this.line + n) - if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n } - return line -}; - -Context.prototype.nextLine = function () { - this.line++ - if (this.maxLookAhead > 0) { this.maxLookAhead-- } -}; - -Context.fromSaved = function (doc, saved, line) { - if (saved instanceof SavedContext) - { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } - else - { return new Context(doc, copyState(doc.mode, saved), line) } -}; - -Context.prototype.save = function (copy) { - var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state - return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state -}; - - -// Compute a style array (an array starting with a mode generation -// -- for invalidation -- followed by pairs of end positions and -// style strings), which is used to highlight the tokens on the -// line. -function highlightLine(cm, line, context, forceToEnd) { - // A styles array always starts with a number identifying the - // mode/overlays that it is based on (for easy invalidation). - var st = [cm.state.modeGen], lineClasses = {} - // Compute the base array of styles - runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, - lineClasses, forceToEnd) - var state = context.state - - // Run overlays, adjust style array. - var loop = function ( o ) { - var overlay = cm.state.overlays[o], i = 1, at = 0 - context.state = true - runMode(cm, line.text, overlay.mode, context, function (end, style) { - var start = i - // Ensure there's a token end at the current position, and that i points at it - while (at < end) { - var i_end = st[i] - if (i_end > end) - { st.splice(i, 1, end, st[i+1], i_end) } - i += 2 - at = Math.min(end, i_end) - } - if (!style) { return } - if (overlay.opaque) { - st.splice(start, i - start, end, "overlay " + style) - i = start + 2 - } else { - for (; start < i; start += 2) { - var cur = st[start+1] - st[start+1] = (cur ? cur + " " : "") + "overlay " + style - } - } - }, lineClasses) - }; - - for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); - context.state = state - - return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} -} - -function getLineStyles(cm, line, updateFrontier) { - if (!line.styles || line.styles[0] != cm.state.modeGen) { - var context = getContextBefore(cm, lineNo(line)) - var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state) - var result = highlightLine(cm, line, context) - if (resetState) { context.state = resetState } - line.stateAfter = context.save(!resetState) - line.styles = result.styles - if (result.classes) { line.styleClasses = result.classes } - else if (line.styleClasses) { line.styleClasses = null } - if (updateFrontier === cm.doc.highlightFrontier) - { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier) } - } - return line.styles -} - -function getContextBefore(cm, n, precise) { - var doc = cm.doc, display = cm.display - if (!doc.mode.startState) { return new Context(doc, true, n) } - var start = findStartLine(cm, n, precise) - var saved = start > doc.first && getLine(doc, start - 1).stateAfter - var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start) - - doc.iter(start, n, function (line) { - processLine(cm, line.text, context) - var pos = context.line - line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null - context.nextLine() - }) - if (precise) { doc.modeFrontier = context.line } - return context -} - -// Lightweight form of highlight -- proceed over this line and -// update state, but don't save a style array. Used for lines that -// aren't currently visible. -function processLine(cm, text, context, startAt) { - var mode = cm.doc.mode - var stream = new StringStream(text, cm.options.tabSize, context) - stream.start = stream.pos = startAt || 0 - if (text == "") { callBlankLine(mode, context.state) } - while (!stream.eol()) { - readToken(mode, stream, context.state) - stream.start = stream.pos - } -} - -function callBlankLine(mode, state) { - if (mode.blankLine) { return mode.blankLine(state) } - if (!mode.innerMode) { return } - var inner = innerMode(mode, state) - if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } -} - -function readToken(mode, stream, state, inner) { - for (var i = 0; i < 10; i++) { - if (inner) { inner[0] = innerMode(mode, state).mode } - var style = mode.token(stream, state) - if (stream.pos > stream.start) { return style } - } - throw new Error("Mode " + mode.name + " failed to advance stream.") -} - -var Token = function(stream, type, state) { - this.start = stream.start; this.end = stream.pos - this.string = stream.current() - this.type = type || null - this.state = state -}; - -// Utility for getTokenAt and getLineTokens -function takeToken(cm, pos, precise, asArray) { - var doc = cm.doc, mode = doc.mode, style - pos = clipPos(doc, pos) - var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise) - var stream = new StringStream(line.text, cm.options.tabSize, context), tokens - if (asArray) { tokens = [] } - while ((asArray || stream.pos < pos.ch) && !stream.eol()) { - stream.start = stream.pos - style = readToken(mode, stream, context.state) - if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))) } - } - return asArray ? tokens : new Token(stream, style, context.state) -} - -function extractLineClasses(type, output) { - if (type) { for (;;) { - var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/) - if (!lineClass) { break } - type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length) - var prop = lineClass[1] ? "bgClass" : "textClass" - if (output[prop] == null) - { output[prop] = lineClass[2] } - else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) - { output[prop] += " " + lineClass[2] } - } } - return type -} - -// Run the given mode's parser over a line, calling f for each token. -function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { - var flattenSpans = mode.flattenSpans - if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans } - var curStart = 0, curStyle = null - var stream = new StringStream(text, cm.options.tabSize, context), style - var inner = cm.options.addModeClass && [null] - if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses) } - while (!stream.eol()) { - if (stream.pos > cm.options.maxHighlightLength) { - flattenSpans = false - if (forceToEnd) { processLine(cm, text, context, stream.pos) } - stream.pos = text.length - style = null - } else { - style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses) - } - if (inner) { - var mName = inner[0].name - if (mName) { style = "m-" + (style ? mName + " " + style : mName) } - } - if (!flattenSpans || curStyle != style) { - while (curStart < stream.start) { - curStart = Math.min(stream.start, curStart + 5000) - f(curStart, curStyle) - } - curStyle = style - } - stream.start = stream.pos - } - while (curStart < stream.pos) { - // Webkit seems to refuse to render text nodes longer than 57444 - // characters, and returns inaccurate measurements in nodes - // starting around 5000 chars. - var pos = Math.min(stream.pos, curStart + 5000) - f(pos, curStyle) - curStart = pos - } -} - -// Finds the line to start with when starting a parse. Tries to -// find a line with a stateAfter, so that it can start with a -// valid state. If that fails, it returns the line with the -// smallest indentation, which tends to need the least context to -// parse correctly. -function findStartLine(cm, n, precise) { - var minindent, minline, doc = cm.doc - var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100) - for (var search = n; search > lim; --search) { - if (search <= doc.first) { return doc.first } - var line = getLine(doc, search - 1), after = line.stateAfter - if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) - { return search } - var indented = countColumn(line.text, null, cm.options.tabSize) - if (minline == null || minindent > indented) { - minline = search - 1 - minindent = indented - } - } - return minline -} - -function retreatFrontier(doc, n) { - doc.modeFrontier = Math.min(doc.modeFrontier, n) - if (doc.highlightFrontier < n - 10) { return } - var start = doc.first - for (var line = n - 1; line > start; line--) { - var saved = getLine(doc, line).stateAfter - // change is on 3 - // state on line 1 looked ahead 2 -- so saw 3 - // test 1 + 2 < 3 should cover this - if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { - start = line + 1 - break - } - } - doc.highlightFrontier = Math.min(doc.highlightFrontier, start) -} - -// LINE DATA STRUCTURE - -// Line objects. These hold state related to a line, including -// highlighting info (the styles array). -var Line = function(text, markedSpans, estimateHeight) { - this.text = text - attachMarkedSpans(this, markedSpans) - this.height = estimateHeight ? estimateHeight(this) : 1 -}; - -Line.prototype.lineNo = function () { return lineNo(this) }; -eventMixin(Line) - -// Change the content (text, markers) of a line. Automatically -// invalidates cached information and tries to re-estimate the -// line's height. -function updateLine(line, text, markedSpans, estimateHeight) { - line.text = text - if (line.stateAfter) { line.stateAfter = null } - if (line.styles) { line.styles = null } - if (line.order != null) { line.order = null } - detachMarkedSpans(line) - attachMarkedSpans(line, markedSpans) - var estHeight = estimateHeight ? estimateHeight(line) : 1 - if (estHeight != line.height) { updateLineHeight(line, estHeight) } -} - -// Detach a line from the document tree and its markers. -function cleanUpLine(line) { - line.parent = null - detachMarkedSpans(line) -} - -// Convert a style as returned by a mode (either null, or a string -// containing one or more styles) to a CSS style. This is cached, -// and also looks for line-wide styles. -var styleToClassCache = {}; -var styleToClassCacheWithMode = {}; -function interpretTokenStyle(style, options) { - if (!style || /^\s*$/.test(style)) { return null } - var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache - return cache[style] || - (cache[style] = style.replace(/\S+/g, "cm-$&")) -} - -// Render the DOM representation of the text of a line. Also builds -// up a 'line map', which points at the DOM nodes that represent -// specific stretches of text, and is used by the measuring code. -// The returned object contains the DOM node, this map, and -// information about line-wide styles that were set by the mode. -function buildLineContent(cm, lineView) { - // The padding-right forces the element to have a 'border', which - // is needed on Webkit to be able to get line-level bounding - // rectangles for it (in measureChar). - var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null) - var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, - col: 0, pos: 0, cm: cm, - trailingSpace: false, - splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")} - lineView.measure = {} - - // Iterate over the logical lines that make up this visual line. - for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { - var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0) - builder.pos = 0 - builder.addToken = buildToken - // Optionally wire in some hacks into the token-rendering - // algorithm, to deal with browser quirks. - if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) - { builder.addToken = buildTokenBadBidi(builder.addToken, order) } - builder.map = [] - var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line) - insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)) - if (line.styleClasses) { - if (line.styleClasses.bgClass) - { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "") } - if (line.styleClasses.textClass) - { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "") } - } - - // Ensure at least a single node is present, for measuring. - if (builder.map.length == 0) - { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))) } - - // Store the map and a cache object for the current logical line - if (i == 0) { - lineView.measure.map = builder.map - lineView.measure.cache = {} - } else { - ;(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) - ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}) - } - } - - // See issue #2901 - if (webkit) { - var last = builder.content.lastChild - if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) - { builder.content.className = "cm-tab-wrap-hack" } - } - - signal(cm, "renderLine", cm, lineView.line, builder.pre) - if (builder.pre.className) - { builder.textClass = joinClasses(builder.pre.className, builder.textClass || "") } - - return builder -} - -function defaultSpecialCharPlaceholder(ch) { - var token = elt("span", "\u2022", "cm-invalidchar") - token.title = "\\u" + ch.charCodeAt(0).toString(16) - token.setAttribute("aria-label", token.title) - return token -} - -// Build up the DOM representation for a single token, and add it to -// the line map. Takes care to render special characters separately. -function buildToken(builder, text, style, startStyle, endStyle, title, css) { - if (!text) { return } - var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text - var special = builder.cm.state.specialChars, mustWrap = false - var content - if (!special.test(text)) { - builder.col += text.length - content = document.createTextNode(displayText) - builder.map.push(builder.pos, builder.pos + text.length, content) - if (ie && ie_version < 9) { mustWrap = true } - builder.pos += text.length - } else { - content = document.createDocumentFragment() - var pos = 0 - while (true) { - special.lastIndex = pos - var m = special.exec(text) - var skipped = m ? m.index - pos : text.length - pos - if (skipped) { - var txt = document.createTextNode(displayText.slice(pos, pos + skipped)) - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])) } - else { content.appendChild(txt) } - builder.map.push(builder.pos, builder.pos + skipped, txt) - builder.col += skipped - builder.pos += skipped - } - if (!m) { break } - pos += skipped + 1 - var txt$1 = (void 0) - if (m[0] == "\t") { - var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize - txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")) - txt$1.setAttribute("role", "presentation") - txt$1.setAttribute("cm-text", "\t") - builder.col += tabWidth - } else if (m[0] == "\r" || m[0] == "\n") { - txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")) - txt$1.setAttribute("cm-text", m[0]) - builder.col += 1 - } else { - txt$1 = builder.cm.options.specialCharPlaceholder(m[0]) - txt$1.setAttribute("cm-text", m[0]) - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])) } - else { content.appendChild(txt$1) } - builder.col += 1 - } - builder.map.push(builder.pos, builder.pos + 1, txt$1) - builder.pos++ - } - } - builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32 - if (style || startStyle || endStyle || mustWrap || css) { - var fullStyle = style || "" - if (startStyle) { fullStyle += startStyle } - if (endStyle) { fullStyle += endStyle } - var token = elt("span", [content], fullStyle, css) - if (title) { token.title = title } - return builder.content.appendChild(token) - } - builder.content.appendChild(content) -} - -function splitSpaces(text, trailingBefore) { - if (text.length > 1 && !/ /.test(text)) { return text } - var spaceBefore = trailingBefore, result = "" - for (var i = 0; i < text.length; i++) { - var ch = text.charAt(i) - if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) - { ch = "\u00a0" } - result += ch - spaceBefore = ch == " " - } - return result -} - -// Work around nonsense dimensions being reported for stretches of -// right-to-left text. -function buildTokenBadBidi(inner, order) { - return function (builder, text, style, startStyle, endStyle, title, css) { - style = style ? style + " cm-force-border" : "cm-force-border" - var start = builder.pos, end = start + text.length - for (;;) { - // Find the part that overlaps with the start of this text - var part = (void 0) - for (var i = 0; i < order.length; i++) { - part = order[i] - if (part.to > start && part.from <= start) { break } - } - if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, title, css) } - inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css) - startStyle = null - text = text.slice(part.to - start) - start = part.to - } - } -} - -function buildCollapsedSpan(builder, size, marker, ignoreWidget) { - var widget = !ignoreWidget && marker.widgetNode - if (widget) { builder.map.push(builder.pos, builder.pos + size, widget) } - if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { - if (!widget) - { widget = builder.content.appendChild(document.createElement("span")) } - widget.setAttribute("cm-marker", marker.id) - } - if (widget) { - builder.cm.display.input.setUneditable(widget) - builder.content.appendChild(widget) - } - builder.pos += size - builder.trailingSpace = false -} - -// Outputs a number of spans to make up a line, taking highlighting -// and marked text into account. -function insertLineContent(line, builder, styles) { - var spans = line.markedSpans, allText = line.text, at = 0 - if (!spans) { - for (var i$1 = 1; i$1 < styles.length; i$1+=2) - { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)) } - return - } - - var len = allText.length, pos = 0, i = 1, text = "", style, css - var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed - for (;;) { - if (nextChange == pos) { // Update current marker set - spanStyle = spanEndStyle = spanStartStyle = title = css = "" - collapsed = null; nextChange = Infinity - var foundBookmarks = [], endStyles = (void 0) - for (var j = 0; j < spans.length; ++j) { - var sp = spans[j], m = sp.marker - if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { - foundBookmarks.push(m) - } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { - if (sp.to != null && sp.to != pos && nextChange > sp.to) { - nextChange = sp.to - spanEndStyle = "" - } - if (m.className) { spanStyle += " " + m.className } - if (m.css) { css = (css ? css + ";" : "") + m.css } - if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle } - if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to) } - if (m.title && !title) { title = m.title } - if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) - { collapsed = sp } - } else if (sp.from > pos && nextChange > sp.from) { - nextChange = sp.from - } - } - if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) - { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1] } } } - - if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) - { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]) } } - if (collapsed && (collapsed.from || 0) == pos) { - buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, - collapsed.marker, collapsed.from == null) - if (collapsed.to == null) { return } - if (collapsed.to == pos) { collapsed = false } - } - } - if (pos >= len) { break } - - var upto = Math.min(len, nextChange) - while (true) { - if (text) { - var end = pos + text.length - if (!collapsed) { - var tokenText = end > upto ? text.slice(0, upto - pos) : text - builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, - spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css) - } - if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} - pos = end - spanStartStyle = "" - } - text = allText.slice(at, at = styles[i++]) - style = interpretTokenStyle(styles[i++], builder.cm.options) - } - } -} - - -// These objects are used to represent the visible (currently drawn) -// part of the document. A LineView may correspond to multiple -// logical lines, if those are connected by collapsed ranges. -function LineView(doc, line, lineN) { - // The starting line - this.line = line - // Continuing lines, if any - this.rest = visualLineContinued(line) - // Number of logical lines in this visual line - this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1 - this.node = this.text = null - this.hidden = lineIsHidden(doc, line) -} - -// Create a range of LineView objects for the given lines. -function buildViewArray(cm, from, to) { - var array = [], nextPos - for (var pos = from; pos < to; pos = nextPos) { - var view = new LineView(cm.doc, getLine(cm.doc, pos), pos) - nextPos = pos + view.size - array.push(view) - } - return array -} - -var operationGroup = null - -function pushOperation(op) { - if (operationGroup) { - operationGroup.ops.push(op) - } else { - op.ownsGroup = operationGroup = { - ops: [op], - delayedCallbacks: [] - } - } -} - -function fireCallbacksForOps(group) { - // Calls delayed callbacks and cursorActivity handlers until no - // new ones appear - var callbacks = group.delayedCallbacks, i = 0 - do { - for (; i < callbacks.length; i++) - { callbacks[i].call(null) } - for (var j = 0; j < group.ops.length; j++) { - var op = group.ops[j] - if (op.cursorActivityHandlers) - { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) - { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm) } } - } - } while (i < callbacks.length) -} - -function finishOperation(op, endCb) { - var group = op.ownsGroup - if (!group) { return } - - try { fireCallbacksForOps(group) } - finally { - operationGroup = null - endCb(group) - } -} - -var orphanDelayedCallbacks = null - -// Often, we want to signal events at a point where we are in the -// middle of some work, but don't want the handler to start calling -// other methods on the editor, which might be in an inconsistent -// state or simply not expect any other events to happen. -// signalLater looks whether there are any handlers, and schedules -// them to be executed when the last operation ends, or, if no -// operation is active, when a timeout fires. -function signalLater(emitter, type /*, values...*/) { - var arr = getHandlers(emitter, type) - if (!arr.length) { return } - var args = Array.prototype.slice.call(arguments, 2), list - if (operationGroup) { - list = operationGroup.delayedCallbacks - } else if (orphanDelayedCallbacks) { - list = orphanDelayedCallbacks - } else { - list = orphanDelayedCallbacks = [] - setTimeout(fireOrphanDelayed, 0) - } - var loop = function ( i ) { - list.push(function () { return arr[i].apply(null, args); }) - }; - - for (var i = 0; i < arr.length; ++i) - loop( i ); -} - -function fireOrphanDelayed() { - var delayed = orphanDelayedCallbacks - orphanDelayedCallbacks = null - for (var i = 0; i < delayed.length; ++i) { delayed[i]() } -} - -// When an aspect of a line changes, a string is added to -// lineView.changes. This updates the relevant part of the line's -// DOM structure. -function updateLineForChanges(cm, lineView, lineN, dims) { - for (var j = 0; j < lineView.changes.length; j++) { - var type = lineView.changes[j] - if (type == "text") { updateLineText(cm, lineView) } - else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims) } - else if (type == "class") { updateLineClasses(cm, lineView) } - else if (type == "widget") { updateLineWidgets(cm, lineView, dims) } - } - lineView.changes = null -} - -// Lines with gutter elements, widgets or a background class need to -// be wrapped, and have the extra elements added to the wrapper div -function ensureLineWrapped(lineView) { - if (lineView.node == lineView.text) { - lineView.node = elt("div", null, null, "position: relative") - if (lineView.text.parentNode) - { lineView.text.parentNode.replaceChild(lineView.node, lineView.text) } - lineView.node.appendChild(lineView.text) - if (ie && ie_version < 8) { lineView.node.style.zIndex = 2 } - } - return lineView.node -} - -function updateLineBackground(cm, lineView) { - var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass - if (cls) { cls += " CodeMirror-linebackground" } - if (lineView.background) { - if (cls) { lineView.background.className = cls } - else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null } - } else if (cls) { - var wrap = ensureLineWrapped(lineView) - lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild) - cm.display.input.setUneditable(lineView.background) - } -} - -// Wrapper around buildLineContent which will reuse the structure -// in display.externalMeasured when possible. -function getLineContent(cm, lineView) { - var ext = cm.display.externalMeasured - if (ext && ext.line == lineView.line) { - cm.display.externalMeasured = null - lineView.measure = ext.measure - return ext.built - } - return buildLineContent(cm, lineView) -} - -// Redraw the line's text. Interacts with the background and text -// classes because the mode may output tokens that influence these -// classes. -function updateLineText(cm, lineView) { - var cls = lineView.text.className - var built = getLineContent(cm, lineView) - if (lineView.text == lineView.node) { lineView.node = built.pre } - lineView.text.parentNode.replaceChild(built.pre, lineView.text) - lineView.text = built.pre - if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { - lineView.bgClass = built.bgClass - lineView.textClass = built.textClass - updateLineClasses(cm, lineView) - } else if (cls) { - lineView.text.className = cls - } -} - -function updateLineClasses(cm, lineView) { - updateLineBackground(cm, lineView) - if (lineView.line.wrapClass) - { ensureLineWrapped(lineView).className = lineView.line.wrapClass } - else if (lineView.node != lineView.text) - { lineView.node.className = "" } - var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass - lineView.text.className = textClass || "" -} - -function updateLineGutter(cm, lineView, lineN, dims) { - if (lineView.gutter) { - lineView.node.removeChild(lineView.gutter) - lineView.gutter = null - } - if (lineView.gutterBackground) { - lineView.node.removeChild(lineView.gutterBackground) - lineView.gutterBackground = null - } - if (lineView.line.gutterClass) { - var wrap = ensureLineWrapped(lineView) - lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, - ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")) - cm.display.input.setUneditable(lineView.gutterBackground) - wrap.insertBefore(lineView.gutterBackground, lineView.text) - } - var markers = lineView.line.gutterMarkers - if (cm.options.lineNumbers || markers) { - var wrap$1 = ensureLineWrapped(lineView) - var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")) - cm.display.input.setUneditable(gutterWrap) - wrap$1.insertBefore(gutterWrap, lineView.text) - if (lineView.line.gutterClass) - { gutterWrap.className += " " + lineView.line.gutterClass } - if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) - { lineView.lineNumber = gutterWrap.appendChild( - elt("div", lineNumberFor(cm.options, lineN), - "CodeMirror-linenumber CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))) } - if (markers) { for (var k = 0; k < cm.options.gutters.length; ++k) { - var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id] - if (found) - { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))) } - } } - } -} - -function updateLineWidgets(cm, lineView, dims) { - if (lineView.alignable) { lineView.alignable = null } - for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { - next = node.nextSibling - if (node.className == "CodeMirror-linewidget") - { lineView.node.removeChild(node) } - } - insertLineWidgets(cm, lineView, dims) -} - -// Build a line's DOM representation from scratch -function buildLineElement(cm, lineView, lineN, dims) { - var built = getLineContent(cm, lineView) - lineView.text = lineView.node = built.pre - if (built.bgClass) { lineView.bgClass = built.bgClass } - if (built.textClass) { lineView.textClass = built.textClass } - - updateLineClasses(cm, lineView) - updateLineGutter(cm, lineView, lineN, dims) - insertLineWidgets(cm, lineView, dims) - return lineView.node -} - -// A lineView may contain multiple logical lines (when merged by -// collapsed spans). The widgets for all of them need to be drawn. -function insertLineWidgets(cm, lineView, dims) { - insertLineWidgetsFor(cm, lineView.line, lineView, dims, true) - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false) } } -} - -function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { - if (!line.widgets) { return } - var wrap = ensureLineWrapped(lineView) - for (var i = 0, ws = line.widgets; i < ws.length; ++i) { - var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget") - if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true") } - positionLineWidget(widget, node, lineView, dims) - cm.display.input.setUneditable(node) - if (allowAbove && widget.above) - { wrap.insertBefore(node, lineView.gutter || lineView.text) } - else - { wrap.appendChild(node) } - signalLater(widget, "redraw") - } -} - -function positionLineWidget(widget, node, lineView, dims) { - if (widget.noHScroll) { - ;(lineView.alignable || (lineView.alignable = [])).push(node) - var width = dims.wrapperWidth - node.style.left = dims.fixedPos + "px" - if (!widget.coverGutter) { - width -= dims.gutterTotalWidth - node.style.paddingLeft = dims.gutterTotalWidth + "px" - } - node.style.width = width + "px" - } - if (widget.coverGutter) { - node.style.zIndex = 5 - node.style.position = "relative" - if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px" } - } -} - -function widgetHeight(widget) { - if (widget.height != null) { return widget.height } - var cm = widget.doc.cm - if (!cm) { return 0 } - if (!contains(document.body, widget.node)) { - var parentStyle = "position: relative;" - if (widget.coverGutter) - { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;" } - if (widget.noHScroll) - { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;" } - removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)) - } - return widget.height = widget.node.parentNode.offsetHeight -} - -// Return true when the given mouse event happened in a widget -function eventInWidget(display, e) { - for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { - if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || - (n.parentNode == display.sizer && n != display.mover)) - { return true } - } -} - -// POSITION MEASUREMENT - -function paddingTop(display) {return display.lineSpace.offsetTop} -function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} -function paddingH(display) { - if (display.cachedPaddingH) { return display.cachedPaddingH } - var e = removeChildrenAndAdd(display.measure, elt("pre", "x")) - var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle - var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)} - if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data } - return data -} - -function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } -function displayWidth(cm) { - return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth -} -function displayHeight(cm) { - return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight -} - -// Ensure the lineView.wrapping.heights array is populated. This is -// an array of bottom offsets for the lines that make up a drawn -// line. When lineWrapping is on, there might be more than one -// height. -function ensureLineHeights(cm, lineView, rect) { - var wrapping = cm.options.lineWrapping - var curWidth = wrapping && displayWidth(cm) - if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { - var heights = lineView.measure.heights = [] - if (wrapping) { - lineView.measure.width = curWidth - var rects = lineView.text.firstChild.getClientRects() - for (var i = 0; i < rects.length - 1; i++) { - var cur = rects[i], next = rects[i + 1] - if (Math.abs(cur.bottom - next.bottom) > 2) - { heights.push((cur.bottom + next.top) / 2 - rect.top) } - } - } - heights.push(rect.bottom - rect.top) - } -} - -// Find a line map (mapping character offsets to text nodes) and a -// measurement cache for the given line number. (A line view might -// contain multiple lines when collapsed ranges are present.) -function mapFromLineView(lineView, line, lineN) { - if (lineView.line == line) - { return {map: lineView.measure.map, cache: lineView.measure.cache} } - for (var i = 0; i < lineView.rest.length; i++) - { if (lineView.rest[i] == line) - { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } - for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) - { if (lineNo(lineView.rest[i$1]) > lineN) - { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } -} - -// Render a line into the hidden node display.externalMeasured. Used -// when measurement is needed for a line that's not in the viewport. -function updateExternalMeasurement(cm, line) { - line = visualLine(line) - var lineN = lineNo(line) - var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN) - view.lineN = lineN - var built = view.built = buildLineContent(cm, view) - view.text = built.pre - removeChildrenAndAdd(cm.display.lineMeasure, built.pre) - return view -} - -// Get a {top, bottom, left, right} box (in line-local coordinates) -// for a given character. -function measureChar(cm, line, ch, bias) { - return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) -} - -// Find a line view that corresponds to the given line number. -function findViewForLine(cm, lineN) { - if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) - { return cm.display.view[findViewIndex(cm, lineN)] } - var ext = cm.display.externalMeasured - if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) - { return ext } -} - -// Measurement can be split in two steps, the set-up work that -// applies to the whole line, and the measurement of the actual -// character. Functions like coordsChar, that need to do a lot of -// measurements in a row, can thus ensure that the set-up work is -// only done once. -function prepareMeasureForLine(cm, line) { - var lineN = lineNo(line) - var view = findViewForLine(cm, lineN) - if (view && !view.text) { - view = null - } else if (view && view.changes) { - updateLineForChanges(cm, view, lineN, getDimensions(cm)) - cm.curOp.forceUpdate = true - } - if (!view) - { view = updateExternalMeasurement(cm, line) } - - var info = mapFromLineView(view, line, lineN) - return { - line: line, view: view, rect: null, - map: info.map, cache: info.cache, before: info.before, - hasHeights: false - } -} - -// Given a prepared measurement object, measures the position of an -// actual character (or fetches it from the cache). -function measureCharPrepared(cm, prepared, ch, bias, varHeight) { - if (prepared.before) { ch = -1 } - var key = ch + (bias || ""), found - if (prepared.cache.hasOwnProperty(key)) { - found = prepared.cache[key] - } else { - if (!prepared.rect) - { prepared.rect = prepared.view.text.getBoundingClientRect() } - if (!prepared.hasHeights) { - ensureLineHeights(cm, prepared.view, prepared.rect) - prepared.hasHeights = true - } - found = measureCharInner(cm, prepared, ch, bias) - if (!found.bogus) { prepared.cache[key] = found } - } - return {left: found.left, right: found.right, - top: varHeight ? found.rtop : found.top, - bottom: varHeight ? found.rbottom : found.bottom} -} - -var nullRect = {left: 0, right: 0, top: 0, bottom: 0} - -function nodeAndOffsetInLineMap(map, ch, bias) { - var node, start, end, collapse, mStart, mEnd - // First, search the line map for the text node corresponding to, - // or closest to, the target character. - for (var i = 0; i < map.length; i += 3) { - mStart = map[i] - mEnd = map[i + 1] - if (ch < mStart) { - start = 0; end = 1 - collapse = "left" - } else if (ch < mEnd) { - start = ch - mStart - end = start + 1 - } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { - end = mEnd - mStart - start = end - 1 - if (ch >= mEnd) { collapse = "right" } - } - if (start != null) { - node = map[i + 2] - if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) - { collapse = bias } - if (bias == "left" && start == 0) - { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { - node = map[(i -= 3) + 2] - collapse = "left" - } } - if (bias == "right" && start == mEnd - mStart) - { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { - node = map[(i += 3) + 2] - collapse = "right" - } } - break - } - } - return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} -} - -function getUsefulRect(rects, bias) { - var rect = nullRect - if (bias == "left") { for (var i = 0; i < rects.length; i++) { - if ((rect = rects[i]).left != rect.right) { break } - } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { - if ((rect = rects[i$1]).left != rect.right) { break } - } } - return rect -} - -function measureCharInner(cm, prepared, ch, bias) { - var place = nodeAndOffsetInLineMap(prepared.map, ch, bias) - var node = place.node, start = place.start, end = place.end, collapse = place.collapse - - var rect - if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. - for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned - while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start } - while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end } - if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) - { rect = node.parentNode.getBoundingClientRect() } - else - { rect = getUsefulRect(range(node, start, end).getClientRects(), bias) } - if (rect.left || rect.right || start == 0) { break } - end = start - start = start - 1 - collapse = "right" - } - if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect) } - } else { // If it is a widget, simply get the box for the whole widget. - if (start > 0) { collapse = bias = "right" } - var rects - if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) - { rect = rects[bias == "right" ? rects.length - 1 : 0] } - else - { rect = node.getBoundingClientRect() } - } - if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { - var rSpan = node.parentNode.getClientRects()[0] - if (rSpan) - { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom} } - else - { rect = nullRect } - } - - var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top - var mid = (rtop + rbot) / 2 - var heights = prepared.view.measure.heights - var i = 0 - for (; i < heights.length - 1; i++) - { if (mid < heights[i]) { break } } - var top = i ? heights[i - 1] : 0, bot = heights[i] - var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, - right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, - top: top, bottom: bot} - if (!rect.left && !rect.right) { result.bogus = true } - if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot } - - return result -} - -// Work around problem with bounding client rects on ranges being -// returned incorrectly when zoomed on IE10 and below. -function maybeUpdateRectForZooming(measure, rect) { - if (!window.screen || screen.logicalXDPI == null || - screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) - { return rect } - var scaleX = screen.logicalXDPI / screen.deviceXDPI - var scaleY = screen.logicalYDPI / screen.deviceYDPI - return {left: rect.left * scaleX, right: rect.right * scaleX, - top: rect.top * scaleY, bottom: rect.bottom * scaleY} -} - -function clearLineMeasurementCacheFor(lineView) { - if (lineView.measure) { - lineView.measure.cache = {} - lineView.measure.heights = null - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { lineView.measure.caches[i] = {} } } - } -} - -function clearLineMeasurementCache(cm) { - cm.display.externalMeasure = null - removeChildren(cm.display.lineMeasure) - for (var i = 0; i < cm.display.view.length; i++) - { clearLineMeasurementCacheFor(cm.display.view[i]) } -} - -function clearCaches(cm) { - clearLineMeasurementCache(cm) - cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null - if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true } - cm.display.lineNumChars = null -} - -function pageScrollX() { - // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 - // which causes page_Offset and bounding client rects to use - // different reference viewports and invalidate our calculations. - if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } - return window.pageXOffset || (document.documentElement || document.body).scrollLeft -} -function pageScrollY() { - if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } - return window.pageYOffset || (document.documentElement || document.body).scrollTop -} - -// Converts a {top, bottom, left, right} box from line-local -// coordinates into another coordinate system. Context may be one of -// "line", "div" (display.lineDiv), "local"./null (editor), "window", -// or "page". -function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { - if (!includeWidgets && lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) { - var size = widgetHeight(lineObj.widgets[i]) - rect.top += size; rect.bottom += size - } } } - if (context == "line") { return rect } - if (!context) { context = "local" } - var yOff = heightAtLine(lineObj) - if (context == "local") { yOff += paddingTop(cm.display) } - else { yOff -= cm.display.viewOffset } - if (context == "page" || context == "window") { - var lOff = cm.display.lineSpace.getBoundingClientRect() - yOff += lOff.top + (context == "window" ? 0 : pageScrollY()) - var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()) - rect.left += xOff; rect.right += xOff - } - rect.top += yOff; rect.bottom += yOff - return rect -} - -// Coverts a box from "div" coords to another coordinate system. -// Context may be "window", "page", "div", or "local"./null. -function fromCoordSystem(cm, coords, context) { - if (context == "div") { return coords } - var left = coords.left, top = coords.top - // First move into "page" coordinate system - if (context == "page") { - left -= pageScrollX() - top -= pageScrollY() - } else if (context == "local" || !context) { - var localBox = cm.display.sizer.getBoundingClientRect() - left += localBox.left - top += localBox.top - } - - var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect() - return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} -} - -function charCoords(cm, pos, context, lineObj, bias) { - if (!lineObj) { lineObj = getLine(cm.doc, pos.line) } - return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) -} - -// Returns a box for a given cursor position, which may have an -// 'other' property containing the position of the secondary cursor -// on a bidi boundary. -// A cursor Pos(line, char, "before") is on the same visual line as `char - 1` -// and after `char - 1` in writing order of `char - 1` -// A cursor Pos(line, char, "after") is on the same visual line as `char` -// and before `char` in writing order of `char` -// Examples (upper-case letters are RTL, lower-case are LTR): -// Pos(0, 1, ...) -// before after -// ab a|b a|b -// aB a|B aB| -// Ab |Ab A|b -// AB B|A B|A -// Every position after the last character on a line is considered to stick -// to the last character on the line. -function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { - lineObj = lineObj || getLine(cm.doc, pos.line) - if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj) } - function get(ch, right) { - var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight) - if (right) { m.left = m.right; } else { m.right = m.left } - return intoCoordSystem(cm, lineObj, m, context) - } - var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky - if (ch >= lineObj.text.length) { - ch = lineObj.text.length - sticky = "before" - } else if (ch <= 0) { - ch = 0 - sticky = "after" - } - if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } - - function getBidi(ch, partPos, invert) { - var part = order[partPos], right = (part.level % 2) != 0 - return get(invert ? ch - 1 : ch, right != invert) - } - var partPos = getBidiPartAt(order, ch, sticky) - var other = bidiOther - var val = getBidi(ch, partPos, sticky == "before") - if (other != null) { val.other = getBidi(ch, other, sticky != "before") } - return val -} - -// Used to cheaply estimate the coordinates for a position. Used for -// intermediate scroll updates. -function estimateCoords(cm, pos) { - var left = 0 - pos = clipPos(cm.doc, pos) - if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch } - var lineObj = getLine(cm.doc, pos.line) - var top = heightAtLine(lineObj) + paddingTop(cm.display) - return {left: left, right: left, top: top, bottom: top + lineObj.height} -} - -// Positions returned by coordsChar contain some extra information. -// xRel is the relative x position of the input coordinates compared -// to the found position (so xRel > 0 means the coordinates are to -// the right of the character position, for example). When outside -// is true, that means the coordinates lie outside the line's -// vertical range. -function PosWithInfo(line, ch, sticky, outside, xRel) { - var pos = Pos(line, ch, sticky) - pos.xRel = xRel - if (outside) { pos.outside = true } - return pos -} - -// Compute the character position closest to the given coordinates. -// Input must be lineSpace-local ("div" coordinate system). -function coordsChar(cm, x, y) { - var doc = cm.doc - y += cm.display.viewOffset - if (y < 0) { return PosWithInfo(doc.first, 0, null, true, -1) } - var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1 - if (lineN > last) - { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) } - if (x < 0) { x = 0 } - - var lineObj = getLine(doc, lineN) - for (;;) { - var found = coordsCharInner(cm, lineObj, lineN, x, y) - var merged = collapsedSpanAtEnd(lineObj) - var mergedPos = merged && merged.find(0, true) - if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) - { lineN = lineNo(lineObj = mergedPos.to.line) } - else - { return found } - } -} - -function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { - var measure = function (ch) { return intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line"); } - var end = lineObj.text.length - var begin = findFirst(function (ch) { return measure(ch - 1).bottom <= y; }, end, 0) - end = findFirst(function (ch) { return measure(ch).top > y; }, begin, end) - return {begin: begin, end: end} -} - -function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { - var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top - return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) -} - -function coordsCharInner(cm, lineObj, lineNo, x, y) { - y -= heightAtLine(lineObj) - var begin = 0, end = lineObj.text.length - var preparedMeasure = prepareMeasureForLine(cm, lineObj) - var pos - var order = getOrder(lineObj, cm.doc.direction) - if (order) { - if (cm.options.lineWrapping) { - ;var assign; - ((assign = wrappedLineExtent(cm, lineObj, preparedMeasure, y), begin = assign.begin, end = assign.end, assign)) - } - pos = new Pos(lineNo, Math.floor(begin + (end - begin) / 2)) - var beginLeft = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left - var dir = beginLeft < x ? 1 : -1 - var prevDiff, diff = beginLeft - x, prevPos - var steps = Math.ceil((end - begin) / 4) - outer: do { - prevDiff = diff - prevPos = pos - var i = 0 - for (; i < steps; ++i) { - var prevPos$1 = pos - pos = moveVisually(cm, lineObj, pos, dir) - if (pos == null || pos.ch < begin || end <= (pos.sticky == "before" ? pos.ch - 1 : pos.ch)) { - pos = prevPos$1 - break outer - } - } - diff = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left - x - if (steps > 1) { - var diff_change_per_step = Math.abs(diff - prevDiff) / steps - steps = Math.min(steps, Math.ceil(Math.abs(diff) / diff_change_per_step)) - dir = diff < 0 ? 1 : -1 - } - } while (diff != 0 && (steps > 1 || ((dir < 0) != (diff < 0) && (Math.abs(diff) <= Math.abs(prevDiff))))) - if (Math.abs(diff) > Math.abs(prevDiff)) { - if ((diff < 0) == (prevDiff < 0)) { throw new Error("Broke out of infinite loop in coordsCharInner") } - pos = prevPos - } - } else { - var ch = findFirst(function (ch) { - var box = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line") - if (box.top > y) { - // For the cursor stickiness - end = Math.min(ch, end) - return true - } - else if (box.bottom <= y) { return false } - else if (box.left > x) { return true } - else if (box.right < x) { return false } - else { return (x - box.left < box.right - x) } - }, begin, end) - ch = skipExtendingChars(lineObj.text, ch, 1) - pos = new Pos(lineNo, ch, ch == end ? "before" : "after") - } - var coords = cursorCoords(cm, pos, "line", lineObj, preparedMeasure) - if (y < coords.top || coords.bottom < y) { pos.outside = true } - pos.xRel = x < coords.left ? -1 : (x > coords.right ? 1 : 0) - return pos -} - -var measureText -// Compute the default text height. -function textHeight(display) { - if (display.cachedTextHeight != null) { return display.cachedTextHeight } - if (measureText == null) { - measureText = elt("pre") - // Measure a bunch of lines, for browsers that compute - // fractional heights. - for (var i = 0; i < 49; ++i) { - measureText.appendChild(document.createTextNode("x")) - measureText.appendChild(elt("br")) - } - measureText.appendChild(document.createTextNode("x")) - } - removeChildrenAndAdd(display.measure, measureText) - var height = measureText.offsetHeight / 50 - if (height > 3) { display.cachedTextHeight = height } - removeChildren(display.measure) - return height || 1 -} - -// Compute the default character width. -function charWidth(display) { - if (display.cachedCharWidth != null) { return display.cachedCharWidth } - var anchor = elt("span", "xxxxxxxxxx") - var pre = elt("pre", [anchor]) - removeChildrenAndAdd(display.measure, pre) - var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10 - if (width > 2) { display.cachedCharWidth = width } - return width || 10 -} - -// Do a bulk-read of the DOM positions and sizes needed to draw the -// view, so that we don't interleave reading and writing to the DOM. -function getDimensions(cm) { - var d = cm.display, left = {}, width = {} - var gutterLeft = d.gutters.clientLeft - for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { - left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft - width[cm.options.gutters[i]] = n.clientWidth - } - return {fixedPos: compensateForHScroll(d), - gutterTotalWidth: d.gutters.offsetWidth, - gutterLeft: left, - gutterWidth: width, - wrapperWidth: d.wrapper.clientWidth} -} - -// Computes display.scroller.scrollLeft + display.gutters.offsetWidth, -// but using getBoundingClientRect to get a sub-pixel-accurate -// result. -function compensateForHScroll(display) { - return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left -} - -// Returns a function that estimates the height of a line, to use as -// first approximation until the line becomes visible (and is thus -// properly measurable). -function estimateHeight(cm) { - var th = textHeight(cm.display), wrapping = cm.options.lineWrapping - var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3) - return function (line) { - if (lineIsHidden(cm.doc, line)) { return 0 } - - var widgetsHeight = 0 - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { - if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height } - } } - - if (wrapping) - { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } - else - { return widgetsHeight + th } - } -} - -function estimateLineHeights(cm) { - var doc = cm.doc, est = estimateHeight(cm) - doc.iter(function (line) { - var estHeight = est(line) - if (estHeight != line.height) { updateLineHeight(line, estHeight) } - }) -} - -// Given a mouse event, find the corresponding position. If liberal -// is false, it checks whether a gutter or scrollbar was clicked, -// and returns null if it was. forRect is used by rectangular -// selections, and tries to estimate a character position even for -// coordinates beyond the right of the text. -function posFromMouse(cm, e, liberal, forRect) { - var display = cm.display - if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } - - var x, y, space = display.lineSpace.getBoundingClientRect() - // Fails unpredictably on IE[67] when mouse is dragged around quickly. - try { x = e.clientX - space.left; y = e.clientY - space.top } - catch (e) { return null } - var coords = coordsChar(cm, x, y), line - if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { - var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length - coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)) - } - return coords -} - -// Find the view element corresponding to a given line. Return null -// when the line isn't visible. -function findViewIndex(cm, n) { - if (n >= cm.display.viewTo) { return null } - n -= cm.display.viewFrom - if (n < 0) { return null } - var view = cm.display.view - for (var i = 0; i < view.length; i++) { - n -= view[i].size - if (n < 0) { return i } - } -} - -function updateSelection(cm) { - cm.display.input.showSelection(cm.display.input.prepareSelection()) -} - -function prepareSelection(cm, primary) { - var doc = cm.doc, result = {} - var curFragment = result.cursors = document.createDocumentFragment() - var selFragment = result.selection = document.createDocumentFragment() - - for (var i = 0; i < doc.sel.ranges.length; i++) { - if (primary === false && i == doc.sel.primIndex) { continue } - var range = doc.sel.ranges[i] - if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } - var collapsed = range.empty() - if (collapsed || cm.options.showCursorWhenSelecting) - { drawSelectionCursor(cm, range.head, curFragment) } - if (!collapsed) - { drawSelectionRange(cm, range, selFragment) } - } - return result -} - -// Draws a cursor for the given range -function drawSelectionCursor(cm, head, output) { - var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine) - - var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")) - cursor.style.left = pos.left + "px" - cursor.style.top = pos.top + "px" - cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px" - - if (pos.other) { - // Secondary cursor, shown when on a 'jump' in bi-directional text - var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")) - otherCursor.style.display = "" - otherCursor.style.left = pos.other.left + "px" - otherCursor.style.top = pos.other.top + "px" - otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px" - } -} - -// Draws the given range as a highlighted selection -function drawSelectionRange(cm, range, output) { - var display = cm.display, doc = cm.doc - var fragment = document.createDocumentFragment() - var padding = paddingH(cm.display), leftSide = padding.left - var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right - - function add(left, top, width, bottom) { - if (top < 0) { top = 0 } - top = Math.round(top) - bottom = Math.round(bottom) - fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))) - } - - function drawForLine(line, fromArg, toArg) { - var lineObj = getLine(doc, line) - var lineLen = lineObj.text.length - var start, end - function coords(ch, bias) { - return charCoords(cm, Pos(line, ch), "div", lineObj, bias) - } - - iterateBidiSections(getOrder(lineObj, doc.direction), fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir) { - var leftPos = coords(from, "left"), rightPos, left, right - if (from == to) { - rightPos = leftPos - left = right = leftPos.left - } else { - rightPos = coords(to - 1, "right") - if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp } - left = leftPos.left - right = rightPos.right - } - if (fromArg == null && from == 0) { left = leftSide } - if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part - add(left, leftPos.top, null, leftPos.bottom) - left = leftSide - if (leftPos.bottom < rightPos.top) { add(left, leftPos.bottom, null, rightPos.top) } - } - if (toArg == null && to == lineLen) { right = rightSide } - if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) - { start = leftPos } - if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) - { end = rightPos } - if (left < leftSide + 1) { left = leftSide } - add(left, rightPos.top, right - left, rightPos.bottom) - }) - return {start: start, end: end} - } - - var sFrom = range.from(), sTo = range.to() - if (sFrom.line == sTo.line) { - drawForLine(sFrom.line, sFrom.ch, sTo.ch) - } else { - var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line) - var singleVLine = visualLine(fromLine) == visualLine(toLine) - var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end - var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start - if (singleVLine) { - if (leftEnd.top < rightStart.top - 2) { - add(leftEnd.right, leftEnd.top, null, leftEnd.bottom) - add(leftSide, rightStart.top, rightStart.left, rightStart.bottom) - } else { - add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom) - } - } - if (leftEnd.bottom < rightStart.top) - { add(leftSide, leftEnd.bottom, null, rightStart.top) } - } - - output.appendChild(fragment) -} - -// Cursor-blinking -function restartBlink(cm) { - if (!cm.state.focused) { return } - var display = cm.display - clearInterval(display.blinker) - var on = true - display.cursorDiv.style.visibility = "" - if (cm.options.cursorBlinkRate > 0) - { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, - cm.options.cursorBlinkRate) } - else if (cm.options.cursorBlinkRate < 0) - { display.cursorDiv.style.visibility = "hidden" } -} - -function ensureFocus(cm) { - if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm) } -} - -function delayBlurEvent(cm) { - cm.state.delayingBlurEvent = true - setTimeout(function () { if (cm.state.delayingBlurEvent) { - cm.state.delayingBlurEvent = false - onBlur(cm) - } }, 100) -} - -function onFocus(cm, e) { - if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false } - - if (cm.options.readOnly == "nocursor") { return } - if (!cm.state.focused) { - signal(cm, "focus", cm, e) - cm.state.focused = true - addClass(cm.display.wrapper, "CodeMirror-focused") - // This test prevents this from firing when a context - // menu is closed (since the input reset would kill the - // select-all detection hack) - if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { - cm.display.input.reset() - if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20) } // Issue #1730 - } - cm.display.input.receivedFocus() - } - restartBlink(cm) -} -function onBlur(cm, e) { - if (cm.state.delayingBlurEvent) { return } - - if (cm.state.focused) { - signal(cm, "blur", cm, e) - cm.state.focused = false - rmClass(cm.display.wrapper, "CodeMirror-focused") - } - clearInterval(cm.display.blinker) - setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false } }, 150) -} - -// Read the actual heights of the rendered lines, and update their -// stored heights to match. -function updateHeightsInViewport(cm) { - var display = cm.display - var prevBottom = display.lineDiv.offsetTop - for (var i = 0; i < display.view.length; i++) { - var cur = display.view[i], height = (void 0) - if (cur.hidden) { continue } - if (ie && ie_version < 8) { - var bot = cur.node.offsetTop + cur.node.offsetHeight - height = bot - prevBottom - prevBottom = bot - } else { - var box = cur.node.getBoundingClientRect() - height = box.bottom - box.top - } - var diff = cur.line.height - height - if (height < 2) { height = textHeight(display) } - if (diff > .005 || diff < -.005) { - updateLineHeight(cur.line, height) - updateWidgetHeight(cur.line) - if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) - { updateWidgetHeight(cur.rest[j]) } } - } - } -} - -// Read and store the height of line widgets associated with the -// given line. -function updateWidgetHeight(line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) - { line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight } } -} - -// Compute the lines that are visible in a given viewport (defaults -// the the current scroll position). viewport may contain top, -// height, and ensure (see op.scrollToPos) properties. -function visibleLines(display, doc, viewport) { - var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop - top = Math.floor(top - paddingTop(display)) - var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight - - var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom) - // Ensure is a {from: {line, ch}, to: {line, ch}} object, and - // forces those lines into the viewport (if possible). - if (viewport && viewport.ensure) { - var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line - if (ensureFrom < from) { - from = ensureFrom - to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight) - } else if (Math.min(ensureTo, doc.lastLine()) >= to) { - from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight) - to = ensureTo - } - } - return {from: from, to: Math.max(to, from + 1)} -} - -// Re-align line numbers and gutter marks to compensate for -// horizontal scrolling. -function alignHorizontally(cm) { - var display = cm.display, view = display.view - if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } - var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft - var gutterW = display.gutters.offsetWidth, left = comp + "px" - for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { - if (cm.options.fixedGutter) { - if (view[i].gutter) - { view[i].gutter.style.left = left } - if (view[i].gutterBackground) - { view[i].gutterBackground.style.left = left } - } - var align = view[i].alignable - if (align) { for (var j = 0; j < align.length; j++) - { align[j].style.left = left } } - } } - if (cm.options.fixedGutter) - { display.gutters.style.left = (comp + gutterW) + "px" } -} - -// Used to ensure that the line number gutter is still the right -// size for the current document size. Returns true when an update -// is needed. -function maybeUpdateLineNumberWidth(cm) { - if (!cm.options.lineNumbers) { return false } - var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display - if (last.length != display.lineNumChars) { - var test = display.measure.appendChild(elt("div", [elt("div", last)], - "CodeMirror-linenumber CodeMirror-gutter-elt")) - var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW - display.lineGutter.style.width = "" - display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1 - display.lineNumWidth = display.lineNumInnerWidth + padding - display.lineNumChars = display.lineNumInnerWidth ? last.length : -1 - display.lineGutter.style.width = display.lineNumWidth + "px" - updateGutterSpace(cm) - return true - } - return false -} - -// SCROLLING THINGS INTO VIEW - -// If an editor sits on the top or bottom of the window, partially -// scrolled out of view, this ensures that the cursor is visible. -function maybeScrollWindow(cm, rect) { - if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } - - var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null - if (rect.top + box.top < 0) { doScroll = true } - else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false } - if (doScroll != null && !phantom) { - var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")) - cm.display.lineSpace.appendChild(scrollNode) - scrollNode.scrollIntoView(doScroll) - cm.display.lineSpace.removeChild(scrollNode) - } -} - -// Scroll a given position into view (immediately), verifying that -// it actually became visible (as line heights are accurately -// measured, the position of something may 'drift' during drawing). -function scrollPosIntoView(cm, pos, end, margin) { - if (margin == null) { margin = 0 } - var rect - if (!cm.options.lineWrapping && pos == end) { - // Set pos and end to the cursor positions around the character pos sticks to - // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch - // If pos == Pos(_, 0, "before"), pos and end are unchanged - pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos - end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos - } - for (var limit = 0; limit < 5; limit++) { - var changed = false - var coords = cursorCoords(cm, pos) - var endCoords = !end || end == pos ? coords : cursorCoords(cm, end) - rect = {left: Math.min(coords.left, endCoords.left), - top: Math.min(coords.top, endCoords.top) - margin, - right: Math.max(coords.left, endCoords.left), - bottom: Math.max(coords.bottom, endCoords.bottom) + margin} - var scrollPos = calculateScrollPos(cm, rect) - var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft - if (scrollPos.scrollTop != null) { - updateScrollTop(cm, scrollPos.scrollTop) - if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true } - } - if (scrollPos.scrollLeft != null) { - setScrollLeft(cm, scrollPos.scrollLeft) - if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true } - } - if (!changed) { break } - } - return rect -} - -// Scroll a given set of coordinates into view (immediately). -function scrollIntoView(cm, rect) { - var scrollPos = calculateScrollPos(cm, rect) - if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop) } - if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft) } -} - -// Calculate a new scroll position needed to scroll the given -// rectangle into view. Returns an object with scrollTop and -// scrollLeft properties. When these are undefined, the -// vertical/horizontal position does not need to be adjusted. -function calculateScrollPos(cm, rect) { - var display = cm.display, snapMargin = textHeight(cm.display) - if (rect.top < 0) { rect.top = 0 } - var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop - var screen = displayHeight(cm), result = {} - if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen } - var docBottom = cm.doc.height + paddingVert(display) - var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin - if (rect.top < screentop) { - result.scrollTop = atTop ? 0 : rect.top - } else if (rect.bottom > screentop + screen) { - var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen) - if (newTop != screentop) { result.scrollTop = newTop } - } - - var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft - var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0) - var tooWide = rect.right - rect.left > screenw - if (tooWide) { rect.right = rect.left + screenw } - if (rect.left < 10) - { result.scrollLeft = 0 } - else if (rect.left < screenleft) - { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)) } - else if (rect.right > screenw + screenleft - 3) - { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw } - return result -} - -// Store a relative adjustment to the scroll position in the current -// operation (to be applied when the operation finishes). -function addToScrollTop(cm, top) { - if (top == null) { return } - resolveScrollToPos(cm) - cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top -} - -// Make sure that at the end of the operation the current cursor is -// shown. -function ensureCursorVisible(cm) { - resolveScrollToPos(cm) - var cur = cm.getCursor() - cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin} -} - -function scrollToCoords(cm, x, y) { - if (x != null || y != null) { resolveScrollToPos(cm) } - if (x != null) { cm.curOp.scrollLeft = x } - if (y != null) { cm.curOp.scrollTop = y } -} - -function scrollToRange(cm, range) { - resolveScrollToPos(cm) - cm.curOp.scrollToPos = range -} - -// When an operation has its scrollToPos property set, and another -// scroll action is applied before the end of the operation, this -// 'simulates' scrolling that position into view in a cheap way, so -// that the effect of intermediate scroll commands is not ignored. -function resolveScrollToPos(cm) { - var range = cm.curOp.scrollToPos - if (range) { - cm.curOp.scrollToPos = null - var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to) - scrollToCoordsRange(cm, from, to, range.margin) - } -} - -function scrollToCoordsRange(cm, from, to, margin) { - var sPos = calculateScrollPos(cm, { - left: Math.min(from.left, to.left), - top: Math.min(from.top, to.top) - margin, - right: Math.max(from.right, to.right), - bottom: Math.max(from.bottom, to.bottom) + margin - }) - scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop) -} - -// Sync the scrollable area and scrollbars, ensure the viewport -// covers the visible area. -function updateScrollTop(cm, val) { - if (Math.abs(cm.doc.scrollTop - val) < 2) { return } - if (!gecko) { updateDisplaySimple(cm, {top: val}) } - setScrollTop(cm, val, true) - if (gecko) { updateDisplaySimple(cm) } - startWorker(cm, 100) -} - -function setScrollTop(cm, val, forceScroll) { - val = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val) - if (cm.display.scroller.scrollTop == val && !forceScroll) { return } - cm.doc.scrollTop = val - cm.display.scrollbars.setScrollTop(val) - if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val } -} - -// Sync scroller and scrollbar, ensure the gutter elements are -// aligned. -function setScrollLeft(cm, val, isScroller, forceScroll) { - val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth) - if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } - cm.doc.scrollLeft = val - alignHorizontally(cm) - if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val } - cm.display.scrollbars.setScrollLeft(val) -} - -// SCROLLBARS - -// Prepare DOM reads needed to update the scrollbars. Done in one -// shot to minimize update/measure roundtrips. -function measureForScrollbars(cm) { - var d = cm.display, gutterW = d.gutters.offsetWidth - var docH = Math.round(cm.doc.height + paddingVert(cm.display)) - return { - clientHeight: d.scroller.clientHeight, - viewHeight: d.wrapper.clientHeight, - scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, - viewWidth: d.wrapper.clientWidth, - barLeft: cm.options.fixedGutter ? gutterW : 0, - docHeight: docH, - scrollHeight: docH + scrollGap(cm) + d.barHeight, - nativeBarWidth: d.nativeBarWidth, - gutterWidth: gutterW - } -} - -var NativeScrollbars = function(place, scroll, cm) { - this.cm = cm - var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar") - var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar") - place(vert); place(horiz) - - on(vert, "scroll", function () { - if (vert.clientHeight) { scroll(vert.scrollTop, "vertical") } - }) - on(horiz, "scroll", function () { - if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal") } - }) - - this.checkedZeroWidth = false - // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px" } -}; - -NativeScrollbars.prototype.update = function (measure) { - var needsH = measure.scrollWidth > measure.clientWidth + 1 - var needsV = measure.scrollHeight > measure.clientHeight + 1 - var sWidth = measure.nativeBarWidth - - if (needsV) { - this.vert.style.display = "block" - this.vert.style.bottom = needsH ? sWidth + "px" : "0" - var totalHeight = measure.viewHeight - (needsH ? sWidth : 0) - // A bug in IE8 can cause this value to be negative, so guard it. - this.vert.firstChild.style.height = - Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px" - } else { - this.vert.style.display = "" - this.vert.firstChild.style.height = "0" - } - - if (needsH) { - this.horiz.style.display = "block" - this.horiz.style.right = needsV ? sWidth + "px" : "0" - this.horiz.style.left = measure.barLeft + "px" - var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0) - this.horiz.firstChild.style.width = - Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px" - } else { - this.horiz.style.display = "" - this.horiz.firstChild.style.width = "0" - } - - if (!this.checkedZeroWidth && measure.clientHeight > 0) { - if (sWidth == 0) { this.zeroWidthHack() } - this.checkedZeroWidth = true - } - - return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} -}; - -NativeScrollbars.prototype.setScrollLeft = function (pos) { - if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos } - if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz") } -}; - -NativeScrollbars.prototype.setScrollTop = function (pos) { - if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos } - if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert") } -}; - -NativeScrollbars.prototype.zeroWidthHack = function () { - var w = mac && !mac_geMountainLion ? "12px" : "18px" - this.horiz.style.height = this.vert.style.width = w - this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none" - this.disableHoriz = new Delayed - this.disableVert = new Delayed -}; - -NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { - bar.style.pointerEvents = "auto" - function maybeDisable() { - // To find out whether the scrollbar is still visible, we - // check whether the element under the pixel in the bottom - // right corner of the scrollbar box is the scrollbar box - // itself (when the bar is still visible) or its filler child - // (when the bar is hidden). If it is still visible, we keep - // it enabled, if it's hidden, we disable pointer events. - var box = bar.getBoundingClientRect() - var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) - : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1) - if (elt != bar) { bar.style.pointerEvents = "none" } - else { delay.set(1000, maybeDisable) } - } - delay.set(1000, maybeDisable) -}; - -NativeScrollbars.prototype.clear = function () { - var parent = this.horiz.parentNode - parent.removeChild(this.horiz) - parent.removeChild(this.vert) -}; - -var NullScrollbars = function () {}; - -NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; -NullScrollbars.prototype.setScrollLeft = function () {}; -NullScrollbars.prototype.setScrollTop = function () {}; -NullScrollbars.prototype.clear = function () {}; - -function updateScrollbars(cm, measure) { - if (!measure) { measure = measureForScrollbars(cm) } - var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight - updateScrollbarsInner(cm, measure) - for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { - if (startWidth != cm.display.barWidth && cm.options.lineWrapping) - { updateHeightsInViewport(cm) } - updateScrollbarsInner(cm, measureForScrollbars(cm)) - startWidth = cm.display.barWidth; startHeight = cm.display.barHeight - } -} - -// Re-synchronize the fake scrollbars with the actual size of the -// content. -function updateScrollbarsInner(cm, measure) { - var d = cm.display - var sizes = d.scrollbars.update(measure) - - d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px" - d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px" - d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent" - - if (sizes.right && sizes.bottom) { - d.scrollbarFiller.style.display = "block" - d.scrollbarFiller.style.height = sizes.bottom + "px" - d.scrollbarFiller.style.width = sizes.right + "px" - } else { d.scrollbarFiller.style.display = "" } - if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { - d.gutterFiller.style.display = "block" - d.gutterFiller.style.height = sizes.bottom + "px" - d.gutterFiller.style.width = measure.gutterWidth + "px" - } else { d.gutterFiller.style.display = "" } -} - -var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars} - -function initScrollbars(cm) { - if (cm.display.scrollbars) { - cm.display.scrollbars.clear() - if (cm.display.scrollbars.addClass) - { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass) } - } - - cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { - cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller) - // Prevent clicks in the scrollbars from killing focus - on(node, "mousedown", function () { - if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0) } - }) - node.setAttribute("cm-not-content", "true") - }, function (pos, axis) { - if (axis == "horizontal") { setScrollLeft(cm, pos) } - else { updateScrollTop(cm, pos) } - }, cm) - if (cm.display.scrollbars.addClass) - { addClass(cm.display.wrapper, cm.display.scrollbars.addClass) } -} - -// Operations are used to wrap a series of changes to the editor -// state in such a way that each change won't have to update the -// cursor and display (which would be awkward, slow, and -// error-prone). Instead, display updates are batched and then all -// combined and executed at once. - -var nextOpId = 0 -// Start a new operation. -function startOperation(cm) { - cm.curOp = { - cm: cm, - viewChanged: false, // Flag that indicates that lines might need to be redrawn - startHeight: cm.doc.height, // Used to detect need to update scrollbar - forceUpdate: false, // Used to force a redraw - updateInput: null, // Whether to reset the input textarea - typing: false, // Whether this reset should be careful to leave existing text (for compositing) - changeObjs: null, // Accumulated changes, for firing change events - cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on - cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already - selectionChanged: false, // Whether the selection needs to be redrawn - updateMaxLine: false, // Set when the widest line needs to be determined anew - scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet - scrollToPos: null, // Used to scroll to a specific position - focus: false, - id: ++nextOpId // Unique ID - } - pushOperation(cm.curOp) -} - -// Finish an operation, updating the display and signalling delayed events -function endOperation(cm) { - var op = cm.curOp - finishOperation(op, function (group) { - for (var i = 0; i < group.ops.length; i++) - { group.ops[i].cm.curOp = null } - endOperations(group) - }) -} - -// The DOM updates done when an operation finishes are batched so -// that the minimum number of relayouts are required. -function endOperations(group) { - var ops = group.ops - for (var i = 0; i < ops.length; i++) // Read DOM - { endOperation_R1(ops[i]) } - for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) - { endOperation_W1(ops[i$1]) } - for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM - { endOperation_R2(ops[i$2]) } - for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) - { endOperation_W2(ops[i$3]) } - for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM - { endOperation_finish(ops[i$4]) } -} - -function endOperation_R1(op) { - var cm = op.cm, display = cm.display - maybeClipScrollbars(cm) - if (op.updateMaxLine) { findMaxLine(cm) } - - op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || - op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || - op.scrollToPos.to.line >= display.viewTo) || - display.maxLineChanged && cm.options.lineWrapping - op.update = op.mustUpdate && - new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate) -} - -function endOperation_W1(op) { - op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update) -} - -function endOperation_R2(op) { - var cm = op.cm, display = cm.display - if (op.updatedDisplay) { updateHeightsInViewport(cm) } - - op.barMeasure = measureForScrollbars(cm) - - // If the max line changed since it was last measured, measure it, - // and ensure the document's width matches it. - // updateDisplay_W2 will use these properties to do the actual resizing - if (display.maxLineChanged && !cm.options.lineWrapping) { - op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3 - cm.display.sizerWidth = op.adjustWidthTo - op.barMeasure.scrollWidth = - Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth) - op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)) - } - - if (op.updatedDisplay || op.selectionChanged) - { op.preparedSelection = display.input.prepareSelection(op.focus) } -} - -function endOperation_W2(op) { - var cm = op.cm - - if (op.adjustWidthTo != null) { - cm.display.sizer.style.minWidth = op.adjustWidthTo + "px" - if (op.maxScrollLeft < cm.doc.scrollLeft) - { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true) } - cm.display.maxLineChanged = false - } - - var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()) - if (op.preparedSelection) - { cm.display.input.showSelection(op.preparedSelection, takeFocus) } - if (op.updatedDisplay || op.startHeight != cm.doc.height) - { updateScrollbars(cm, op.barMeasure) } - if (op.updatedDisplay) - { setDocumentHeight(cm, op.barMeasure) } - - if (op.selectionChanged) { restartBlink(cm) } - - if (cm.state.focused && op.updateInput) - { cm.display.input.reset(op.typing) } - if (takeFocus) { ensureFocus(op.cm) } -} - -function endOperation_finish(op) { - var cm = op.cm, display = cm.display, doc = cm.doc - - if (op.updatedDisplay) { postUpdateDisplay(cm, op.update) } - - // Abort mouse wheel delta measurement, when scrolling explicitly - if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) - { display.wheelStartX = display.wheelStartY = null } - - // Propagate the scroll position to the actual DOM scroller - if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll) } - - if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true) } - // If we need to scroll a specific position into view, do so. - if (op.scrollToPos) { - var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), - clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin) - maybeScrollWindow(cm, rect) - } - - // Fire events for markers that are hidden/unidden by editing or - // undoing - var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers - if (hidden) { for (var i = 0; i < hidden.length; ++i) - { if (!hidden[i].lines.length) { signal(hidden[i], "hide") } } } - if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) - { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide") } } } - - if (display.wrapper.offsetHeight) - { doc.scrollTop = cm.display.scroller.scrollTop } - - // Fire change events, and delayed event handlers - if (op.changeObjs) - { signal(cm, "changes", cm, op.changeObjs) } - if (op.update) - { op.update.finish() } -} - -// Run the given function in an operation -function runInOp(cm, f) { - if (cm.curOp) { return f() } - startOperation(cm) - try { return f() } - finally { endOperation(cm) } -} -// Wraps a function in an operation. Returns the wrapped function. -function operation(cm, f) { - return function() { - if (cm.curOp) { return f.apply(cm, arguments) } - startOperation(cm) - try { return f.apply(cm, arguments) } - finally { endOperation(cm) } - } -} -// Used to add methods to editor and doc instances, wrapping them in -// operations. -function methodOp(f) { - return function() { - if (this.curOp) { return f.apply(this, arguments) } - startOperation(this) - try { return f.apply(this, arguments) } - finally { endOperation(this) } - } -} -function docMethodOp(f) { - return function() { - var cm = this.cm - if (!cm || cm.curOp) { return f.apply(this, arguments) } - startOperation(cm) - try { return f.apply(this, arguments) } - finally { endOperation(cm) } - } -} - -// Updates the display.view data structure for a given change to the -// document. From and to are in pre-change coordinates. Lendiff is -// the amount of lines added or subtracted by the change. This is -// used for changes that span multiple lines, or change the way -// lines are divided into visual lines. regLineChange (below) -// registers single-line changes. -function regChange(cm, from, to, lendiff) { - if (from == null) { from = cm.doc.first } - if (to == null) { to = cm.doc.first + cm.doc.size } - if (!lendiff) { lendiff = 0 } - - var display = cm.display - if (lendiff && to < display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers > from)) - { display.updateLineNumbers = from } - - cm.curOp.viewChanged = true - - if (from >= display.viewTo) { // Change after - if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) - { resetView(cm) } - } else if (to <= display.viewFrom) { // Change before - if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { - resetView(cm) - } else { - display.viewFrom += lendiff - display.viewTo += lendiff - } - } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap - resetView(cm) - } else if (from <= display.viewFrom) { // Top overlap - var cut = viewCuttingPoint(cm, to, to + lendiff, 1) - if (cut) { - display.view = display.view.slice(cut.index) - display.viewFrom = cut.lineN - display.viewTo += lendiff - } else { - resetView(cm) - } - } else if (to >= display.viewTo) { // Bottom overlap - var cut$1 = viewCuttingPoint(cm, from, from, -1) - if (cut$1) { - display.view = display.view.slice(0, cut$1.index) - display.viewTo = cut$1.lineN - } else { - resetView(cm) - } - } else { // Gap in the middle - var cutTop = viewCuttingPoint(cm, from, from, -1) - var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1) - if (cutTop && cutBot) { - display.view = display.view.slice(0, cutTop.index) - .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) - .concat(display.view.slice(cutBot.index)) - display.viewTo += lendiff - } else { - resetView(cm) - } - } - - var ext = display.externalMeasured - if (ext) { - if (to < ext.lineN) - { ext.lineN += lendiff } - else if (from < ext.lineN + ext.size) - { display.externalMeasured = null } - } -} - -// Register a change to a single line. Type must be one of "text", -// "gutter", "class", "widget" -function regLineChange(cm, line, type) { - cm.curOp.viewChanged = true - var display = cm.display, ext = cm.display.externalMeasured - if (ext && line >= ext.lineN && line < ext.lineN + ext.size) - { display.externalMeasured = null } - - if (line < display.viewFrom || line >= display.viewTo) { return } - var lineView = display.view[findViewIndex(cm, line)] - if (lineView.node == null) { return } - var arr = lineView.changes || (lineView.changes = []) - if (indexOf(arr, type) == -1) { arr.push(type) } -} - -// Clear the view. -function resetView(cm) { - cm.display.viewFrom = cm.display.viewTo = cm.doc.first - cm.display.view = [] - cm.display.viewOffset = 0 -} - -function viewCuttingPoint(cm, oldN, newN, dir) { - var index = findViewIndex(cm, oldN), diff, view = cm.display.view - if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) - { return {index: index, lineN: newN} } - var n = cm.display.viewFrom - for (var i = 0; i < index; i++) - { n += view[i].size } - if (n != oldN) { - if (dir > 0) { - if (index == view.length - 1) { return null } - diff = (n + view[index].size) - oldN - index++ - } else { - diff = n - oldN - } - oldN += diff; newN += diff - } - while (visualLineNo(cm.doc, newN) != newN) { - if (index == (dir < 0 ? 0 : view.length - 1)) { return null } - newN += dir * view[index - (dir < 0 ? 1 : 0)].size - index += dir - } - return {index: index, lineN: newN} -} - -// Force the view to cover a given range, adding empty view element -// or clipping off existing ones as needed. -function adjustView(cm, from, to) { - var display = cm.display, view = display.view - if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { - display.view = buildViewArray(cm, from, to) - display.viewFrom = from - } else { - if (display.viewFrom > from) - { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view) } - else if (display.viewFrom < from) - { display.view = display.view.slice(findViewIndex(cm, from)) } - display.viewFrom = from - if (display.viewTo < to) - { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)) } - else if (display.viewTo > to) - { display.view = display.view.slice(0, findViewIndex(cm, to)) } - } - display.viewTo = to -} - -// Count the number of lines in the view whose DOM representation is -// out of date (or nonexistent). -function countDirtyView(cm) { - var view = cm.display.view, dirty = 0 - for (var i = 0; i < view.length; i++) { - var lineView = view[i] - if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty } - } - return dirty -} - -// HIGHLIGHT WORKER - -function startWorker(cm, time) { - if (cm.doc.highlightFrontier < cm.display.viewTo) - { cm.state.highlight.set(time, bind(highlightWorker, cm)) } -} - -function highlightWorker(cm) { - var doc = cm.doc - if (doc.highlightFrontier >= cm.display.viewTo) { return } - var end = +new Date + cm.options.workTime - var context = getContextBefore(cm, doc.highlightFrontier) - var changedLines = [] - - doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { - if (context.line >= cm.display.viewFrom) { // Visible - var oldStyles = line.styles - var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null - var highlighted = highlightLine(cm, line, context, true) - if (resetState) { context.state = resetState } - line.styles = highlighted.styles - var oldCls = line.styleClasses, newCls = highlighted.classes - if (newCls) { line.styleClasses = newCls } - else if (oldCls) { line.styleClasses = null } - var ischange = !oldStyles || oldStyles.length != line.styles.length || - oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass) - for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i] } - if (ischange) { changedLines.push(context.line) } - line.stateAfter = context.save() - context.nextLine() - } else { - if (line.text.length <= cm.options.maxHighlightLength) - { processLine(cm, line.text, context) } - line.stateAfter = context.line % 5 == 0 ? context.save() : null - context.nextLine() - } - if (+new Date > end) { - startWorker(cm, cm.options.workDelay) - return true - } - }) - doc.highlightFrontier = context.line - doc.modeFrontier = Math.max(doc.modeFrontier, context.line) - if (changedLines.length) { runInOp(cm, function () { - for (var i = 0; i < changedLines.length; i++) - { regLineChange(cm, changedLines[i], "text") } - }) } -} - -// DISPLAY DRAWING - -var DisplayUpdate = function(cm, viewport, force) { - var display = cm.display - - this.viewport = viewport - // Store some values that we'll need later (but don't want to force a relayout for) - this.visible = visibleLines(display, cm.doc, viewport) - this.editorIsHidden = !display.wrapper.offsetWidth - this.wrapperHeight = display.wrapper.clientHeight - this.wrapperWidth = display.wrapper.clientWidth - this.oldDisplayWidth = displayWidth(cm) - this.force = force - this.dims = getDimensions(cm) - this.events = [] -}; - -DisplayUpdate.prototype.signal = function (emitter, type) { - if (hasHandler(emitter, type)) - { this.events.push(arguments) } -}; -DisplayUpdate.prototype.finish = function () { - var this$1 = this; - - for (var i = 0; i < this.events.length; i++) - { signal.apply(null, this$1.events[i]) } -}; - -function maybeClipScrollbars(cm) { - var display = cm.display - if (!display.scrollbarsClipped && display.scroller.offsetWidth) { - display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth - display.heightForcer.style.height = scrollGap(cm) + "px" - display.sizer.style.marginBottom = -display.nativeBarWidth + "px" - display.sizer.style.borderRightWidth = scrollGap(cm) + "px" - display.scrollbarsClipped = true - } -} - -function selectionSnapshot(cm) { - if (cm.hasFocus()) { return null } - var active = activeElt() - if (!active || !contains(cm.display.lineDiv, active)) { return null } - var result = {activeElt: active} - if (window.getSelection) { - var sel = window.getSelection() - if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { - result.anchorNode = sel.anchorNode - result.anchorOffset = sel.anchorOffset - result.focusNode = sel.focusNode - result.focusOffset = sel.focusOffset - } - } - return result -} - -function restoreSelection(snapshot) { - if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } - snapshot.activeElt.focus() - if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { - var sel = window.getSelection(), range = document.createRange() - range.setEnd(snapshot.anchorNode, snapshot.anchorOffset) - range.collapse(false) - sel.removeAllRanges() - sel.addRange(range) - sel.extend(snapshot.focusNode, snapshot.focusOffset) - } -} - -// Does the actual updating of the line display. Bails out -// (returning false) when there is nothing to be done and forced is -// false. -function updateDisplayIfNeeded(cm, update) { - var display = cm.display, doc = cm.doc - - if (update.editorIsHidden) { - resetView(cm) - return false - } - - // Bail out if the visible area is already rendered and nothing changed. - if (!update.force && - update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && - display.renderedView == display.view && countDirtyView(cm) == 0) - { return false } - - if (maybeUpdateLineNumberWidth(cm)) { - resetView(cm) - update.dims = getDimensions(cm) - } - - // Compute a suitable new viewport (from & to) - var end = doc.first + doc.size - var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first) - var to = Math.min(end, update.visible.to + cm.options.viewportMargin) - if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom) } - if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo) } - if (sawCollapsedSpans) { - from = visualLineNo(cm.doc, from) - to = visualLineEndNo(cm.doc, to) - } - - var different = from != display.viewFrom || to != display.viewTo || - display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth - adjustView(cm, from, to) - - display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)) - // Position the mover div to align with the current scroll position - cm.display.mover.style.top = display.viewOffset + "px" - - var toUpdate = countDirtyView(cm) - if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) - { return false } - - // For big changes, we hide the enclosing element during the - // update, since that speeds up the operations on most browsers. - var selSnapshot = selectionSnapshot(cm) - if (toUpdate > 4) { display.lineDiv.style.display = "none" } - patchDisplay(cm, display.updateLineNumbers, update.dims) - if (toUpdate > 4) { display.lineDiv.style.display = "" } - display.renderedView = display.view - // There might have been a widget with a focused element that got - // hidden or updated, if so re-focus it. - restoreSelection(selSnapshot) - - // Prevent selection and cursors from interfering with the scroll - // width and height. - removeChildren(display.cursorDiv) - removeChildren(display.selectionDiv) - display.gutters.style.height = display.sizer.style.minHeight = 0 - - if (different) { - display.lastWrapHeight = update.wrapperHeight - display.lastWrapWidth = update.wrapperWidth - startWorker(cm, 400) - } - - display.updateLineNumbers = null - - return true -} - -function postUpdateDisplay(cm, update) { - var viewport = update.viewport - - for (var first = true;; first = false) { - if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { - // Clip forced viewport to actual scrollable area. - if (viewport && viewport.top != null) - { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)} } - // Updated line heights might result in the drawn area not - // actually covering the viewport. Keep looping until it does. - update.visible = visibleLines(cm.display, cm.doc, viewport) - if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) - { break } - } - if (!updateDisplayIfNeeded(cm, update)) { break } - updateHeightsInViewport(cm) - var barMeasure = measureForScrollbars(cm) - updateSelection(cm) - updateScrollbars(cm, barMeasure) - setDocumentHeight(cm, barMeasure) - update.force = false - } - - update.signal(cm, "update", cm) - if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { - update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo) - cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo - } -} - -function updateDisplaySimple(cm, viewport) { - var update = new DisplayUpdate(cm, viewport) - if (updateDisplayIfNeeded(cm, update)) { - updateHeightsInViewport(cm) - postUpdateDisplay(cm, update) - var barMeasure = measureForScrollbars(cm) - updateSelection(cm) - updateScrollbars(cm, barMeasure) - setDocumentHeight(cm, barMeasure) - update.finish() - } -} - -// Sync the actual display DOM structure with display.view, removing -// nodes for lines that are no longer in view, and creating the ones -// that are not there yet, and updating the ones that are out of -// date. -function patchDisplay(cm, updateNumbersFrom, dims) { - var display = cm.display, lineNumbers = cm.options.lineNumbers - var container = display.lineDiv, cur = container.firstChild - - function rm(node) { - var next = node.nextSibling - // Works around a throw-scroll bug in OS X Webkit - if (webkit && mac && cm.display.currentWheelTarget == node) - { node.style.display = "none" } - else - { node.parentNode.removeChild(node) } - return next - } - - var view = display.view, lineN = display.viewFrom - // Loop over the elements in the view, syncing cur (the DOM nodes - // in display.lineDiv) with the view as we go. - for (var i = 0; i < view.length; i++) { - var lineView = view[i] - if (lineView.hidden) { - } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet - var node = buildLineElement(cm, lineView, lineN, dims) - container.insertBefore(node, cur) - } else { // Already drawn - while (cur != lineView.node) { cur = rm(cur) } - var updateNumber = lineNumbers && updateNumbersFrom != null && - updateNumbersFrom <= lineN && lineView.lineNumber - if (lineView.changes) { - if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false } - updateLineForChanges(cm, lineView, lineN, dims) - } - if (updateNumber) { - removeChildren(lineView.lineNumber) - lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))) - } - cur = lineView.node.nextSibling - } - lineN += lineView.size - } - while (cur) { cur = rm(cur) } -} - -function updateGutterSpace(cm) { - var width = cm.display.gutters.offsetWidth - cm.display.sizer.style.marginLeft = width + "px" -} - -function setDocumentHeight(cm, measure) { - cm.display.sizer.style.minHeight = measure.docHeight + "px" - cm.display.heightForcer.style.top = measure.docHeight + "px" - cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px" -} - -// Rebuild the gutter elements, ensure the margin to the left of the -// code matches their width. -function updateGutters(cm) { - var gutters = cm.display.gutters, specs = cm.options.gutters - removeChildren(gutters) - var i = 0 - for (; i < specs.length; ++i) { - var gutterClass = specs[i] - var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)) - if (gutterClass == "CodeMirror-linenumbers") { - cm.display.lineGutter = gElt - gElt.style.width = (cm.display.lineNumWidth || 1) + "px" - } - } - gutters.style.display = i ? "" : "none" - updateGutterSpace(cm) -} - -// Make sure the gutters options contains the element -// "CodeMirror-linenumbers" when the lineNumbers option is true. -function setGuttersForLineNumbers(options) { - var found = indexOf(options.gutters, "CodeMirror-linenumbers") - if (found == -1 && options.lineNumbers) { - options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]) - } else if (found > -1 && !options.lineNumbers) { - options.gutters = options.gutters.slice(0) - options.gutters.splice(found, 1) - } -} - -var wheelSamples = 0; -var wheelPixelsPerUnit = null; -// Fill in a browser-detected starting value on browsers where we -// know one. These don't have to be accurate -- the result of them -// being wrong would just be a slight flicker on the first wheel -// scroll (if it is large enough). -if (ie) { wheelPixelsPerUnit = -.53 } -else if (gecko) { wheelPixelsPerUnit = 15 } -else if (chrome) { wheelPixelsPerUnit = -.7 } -else if (safari) { wheelPixelsPerUnit = -1/3 } - -function wheelEventDelta(e) { - var dx = e.wheelDeltaX, dy = e.wheelDeltaY - if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail } - if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail } - else if (dy == null) { dy = e.wheelDelta } - return {x: dx, y: dy} -} -function wheelEventPixels(e) { - var delta = wheelEventDelta(e) - delta.x *= wheelPixelsPerUnit - delta.y *= wheelPixelsPerUnit - return delta -} - -function onScrollWheel(cm, e) { - var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y - - var display = cm.display, scroll = display.scroller - // Quit if there's nothing to scroll here - var canScrollX = scroll.scrollWidth > scroll.clientWidth - var canScrollY = scroll.scrollHeight > scroll.clientHeight - if (!(dx && canScrollX || dy && canScrollY)) { return } - - // Webkit browsers on OS X abort momentum scrolls when the target - // of the scroll event is removed from the scrollable element. - // This hack (see related code in patchDisplay) makes sure the - // element is kept around. - if (dy && mac && webkit) { - outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { - for (var i = 0; i < view.length; i++) { - if (view[i].node == cur) { - cm.display.currentWheelTarget = cur - break outer - } - } - } - } - - // On some browsers, horizontal scrolling will cause redraws to - // happen before the gutter has been realigned, causing it to - // wriggle around in a most unseemly way. When we have an - // estimated pixels/delta value, we just handle horizontal - // scrolling entirely here. It'll be slightly off from native, but - // better than glitching out. - if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { - if (dy && canScrollY) - { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)) } - setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)) - // Only prevent default scrolling if vertical scrolling is - // actually possible. Otherwise, it causes vertical scroll - // jitter on OSX trackpads when deltaX is small and deltaY - // is large (issue #3579) - if (!dy || (dy && canScrollY)) - { e_preventDefault(e) } - display.wheelStartX = null // Abort measurement, if in progress - return - } - - // 'Project' the visible viewport to cover the area that is being - // scrolled into view (if we know enough to estimate it). - if (dy && wheelPixelsPerUnit != null) { - var pixels = dy * wheelPixelsPerUnit - var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight - if (pixels < 0) { top = Math.max(0, top + pixels - 50) } - else { bot = Math.min(cm.doc.height, bot + pixels + 50) } - updateDisplaySimple(cm, {top: top, bottom: bot}) - } - - if (wheelSamples < 20) { - if (display.wheelStartX == null) { - display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop - display.wheelDX = dx; display.wheelDY = dy - setTimeout(function () { - if (display.wheelStartX == null) { return } - var movedX = scroll.scrollLeft - display.wheelStartX - var movedY = scroll.scrollTop - display.wheelStartY - var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || - (movedX && display.wheelDX && movedX / display.wheelDX) - display.wheelStartX = display.wheelStartY = null - if (!sample) { return } - wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1) - ++wheelSamples - }, 200) - } else { - display.wheelDX += dx; display.wheelDY += dy - } - } -} - -// Selection objects are immutable. A new one is created every time -// the selection changes. A selection is one or more non-overlapping -// (and non-touching) ranges, sorted, and an integer that indicates -// which one is the primary selection (the one that's scrolled into -// view, that getCursor returns, etc). -var Selection = function(ranges, primIndex) { - this.ranges = ranges - this.primIndex = primIndex -}; - -Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; - -Selection.prototype.equals = function (other) { - var this$1 = this; - - if (other == this) { return true } - if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } - for (var i = 0; i < this.ranges.length; i++) { - var here = this$1.ranges[i], there = other.ranges[i] - if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } - } - return true -}; - -Selection.prototype.deepCopy = function () { - var this$1 = this; - - var out = [] - for (var i = 0; i < this.ranges.length; i++) - { out[i] = new Range(copyPos(this$1.ranges[i].anchor), copyPos(this$1.ranges[i].head)) } - return new Selection(out, this.primIndex) -}; - -Selection.prototype.somethingSelected = function () { - var this$1 = this; - - for (var i = 0; i < this.ranges.length; i++) - { if (!this$1.ranges[i].empty()) { return true } } - return false -}; - -Selection.prototype.contains = function (pos, end) { - var this$1 = this; - - if (!end) { end = pos } - for (var i = 0; i < this.ranges.length; i++) { - var range = this$1.ranges[i] - if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) - { return i } - } - return -1 -}; - -var Range = function(anchor, head) { - this.anchor = anchor; this.head = head -}; - -Range.prototype.from = function () { return minPos(this.anchor, this.head) }; -Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; -Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; - -// Take an unsorted, potentially overlapping set of ranges, and -// build a selection out of it. 'Consumes' ranges array (modifying -// it). -function normalizeSelection(ranges, primIndex) { - var prim = ranges[primIndex] - ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }) - primIndex = indexOf(ranges, prim) - for (var i = 1; i < ranges.length; i++) { - var cur = ranges[i], prev = ranges[i - 1] - if (cmp(prev.to(), cur.from()) >= 0) { - var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()) - var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head - if (i <= primIndex) { --primIndex } - ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)) - } - } - return new Selection(ranges, primIndex) -} - -function simpleSelection(anchor, head) { - return new Selection([new Range(anchor, head || anchor)], 0) -} - -// Compute the position of the end of a change (its 'to' property -// refers to the pre-change end). -function changeEnd(change) { - if (!change.text) { return change.to } - return Pos(change.from.line + change.text.length - 1, - lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) -} - -// Adjust a position to refer to the post-change position of the -// same text, or the end of the change if the change covers it. -function adjustForChange(pos, change) { - if (cmp(pos, change.from) < 0) { return pos } - if (cmp(pos, change.to) <= 0) { return changeEnd(change) } - - var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch - if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch } - return Pos(line, ch) -} - -function computeSelAfterChange(doc, change) { - var out = [] - for (var i = 0; i < doc.sel.ranges.length; i++) { - var range = doc.sel.ranges[i] - out.push(new Range(adjustForChange(range.anchor, change), - adjustForChange(range.head, change))) - } - return normalizeSelection(out, doc.sel.primIndex) -} - -function offsetPos(pos, old, nw) { - if (pos.line == old.line) - { return Pos(nw.line, pos.ch - old.ch + nw.ch) } - else - { return Pos(nw.line + (pos.line - old.line), pos.ch) } -} - -// Used by replaceSelections to allow moving the selection to the -// start or around the replaced test. Hint may be "start" or "around". -function computeReplacedSel(doc, changes, hint) { - var out = [] - var oldPrev = Pos(doc.first, 0), newPrev = oldPrev - for (var i = 0; i < changes.length; i++) { - var change = changes[i] - var from = offsetPos(change.from, oldPrev, newPrev) - var to = offsetPos(changeEnd(change), oldPrev, newPrev) - oldPrev = change.to - newPrev = to - if (hint == "around") { - var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0 - out[i] = new Range(inv ? to : from, inv ? from : to) - } else { - out[i] = new Range(from, from) - } - } - return new Selection(out, doc.sel.primIndex) -} - -// Used to get the editor into a consistent state again when options change. - -function loadMode(cm) { - cm.doc.mode = getMode(cm.options, cm.doc.modeOption) - resetModeState(cm) -} - -function resetModeState(cm) { - cm.doc.iter(function (line) { - if (line.stateAfter) { line.stateAfter = null } - if (line.styles) { line.styles = null } - }) - cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first - startWorker(cm, 100) - cm.state.modeGen++ - if (cm.curOp) { regChange(cm) } -} - -// DOCUMENT DATA STRUCTURE - -// By default, updates that start and end at the beginning of a line -// are treated specially, in order to make the association of line -// widgets and marker elements with the text behave more intuitive. -function isWholeLineUpdate(doc, change) { - return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && - (!doc.cm || doc.cm.options.wholeLineUpdateBefore) -} - -// Perform a change on the document data structure. -function updateDoc(doc, change, markedSpans, estimateHeight) { - function spansFor(n) {return markedSpans ? markedSpans[n] : null} - function update(line, text, spans) { - updateLine(line, text, spans, estimateHeight) - signalLater(line, "change", line, change) - } - function linesFor(start, end) { - var result = [] - for (var i = start; i < end; ++i) - { result.push(new Line(text[i], spansFor(i), estimateHeight)) } - return result - } - - var from = change.from, to = change.to, text = change.text - var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line) - var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line - - // Adjust the line structure - if (change.full) { - doc.insert(0, linesFor(0, text.length)) - doc.remove(text.length, doc.size - text.length) - } else if (isWholeLineUpdate(doc, change)) { - // This is a whole-line replace. Treated specially to make - // sure line objects move the way they are supposed to. - var added = linesFor(0, text.length - 1) - update(lastLine, lastLine.text, lastSpans) - if (nlines) { doc.remove(from.line, nlines) } - if (added.length) { doc.insert(from.line, added) } - } else if (firstLine == lastLine) { - if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans) - } else { - var added$1 = linesFor(1, text.length - 1) - added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)) - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)) - doc.insert(from.line + 1, added$1) - } - } else if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)) - doc.remove(from.line + 1, nlines) - } else { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)) - update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans) - var added$2 = linesFor(1, text.length - 1) - if (nlines > 1) { doc.remove(from.line + 1, nlines - 1) } - doc.insert(from.line + 1, added$2) - } - - signalLater(doc, "change", doc, change) -} - -// Call f for all linked documents. -function linkedDocs(doc, f, sharedHistOnly) { - function propagate(doc, skip, sharedHist) { - if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { - var rel = doc.linked[i] - if (rel.doc == skip) { continue } - var shared = sharedHist && rel.sharedHist - if (sharedHistOnly && !shared) { continue } - f(rel.doc, shared) - propagate(rel.doc, doc, shared) - } } - } - propagate(doc, null, true) -} - -// Attach a document to an editor. -function attachDoc(cm, doc) { - if (doc.cm) { throw new Error("This document is already in use.") } - cm.doc = doc - doc.cm = cm - estimateLineHeights(cm) - loadMode(cm) - setDirectionClass(cm) - if (!cm.options.lineWrapping) { findMaxLine(cm) } - cm.options.mode = doc.modeOption - regChange(cm) -} - -function setDirectionClass(cm) { - ;(cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl") -} - -function directionChanged(cm) { - runInOp(cm, function () { - setDirectionClass(cm) - regChange(cm) - }) -} - -function History(startGen) { - // Arrays of change events and selections. Doing something adds an - // event to done and clears undo. Undoing moves events from done - // to undone, redoing moves them in the other direction. - this.done = []; this.undone = [] - this.undoDepth = Infinity - // Used to track when changes can be merged into a single undo - // event - this.lastModTime = this.lastSelTime = 0 - this.lastOp = this.lastSelOp = null - this.lastOrigin = this.lastSelOrigin = null - // Used by the isClean() method - this.generation = this.maxGeneration = startGen || 1 -} - -// Create a history change event from an updateDoc-style change -// object. -function historyChangeFromChange(doc, change) { - var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)} - attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1) - linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true) - return histChange -} - -// Pop all selection events off the end of a history array. Stop at -// a change event. -function clearSelectionEvents(array) { - while (array.length) { - var last = lst(array) - if (last.ranges) { array.pop() } - else { break } - } -} - -// Find the top change event in the history. Pop off selection -// events that are in the way. -function lastChangeEvent(hist, force) { - if (force) { - clearSelectionEvents(hist.done) - return lst(hist.done) - } else if (hist.done.length && !lst(hist.done).ranges) { - return lst(hist.done) - } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { - hist.done.pop() - return lst(hist.done) - } -} - -// Register a change in the history. Merges changes that are within -// a single operation, or are close together with an origin that -// allows merging (starting with "+") into a single event. -function addChangeToHistory(doc, change, selAfter, opId) { - var hist = doc.history - hist.undone.length = 0 - var time = +new Date, cur - var last - - if ((hist.lastOp == opId || - hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || - change.origin.charAt(0) == "*")) && - (cur = lastChangeEvent(hist, hist.lastOp == opId))) { - // Merge this change into the last event - last = lst(cur.changes) - if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { - // Optimized case for simple insertion -- don't want to add - // new changesets for every character typed - last.to = changeEnd(change) - } else { - // Add new sub-event - cur.changes.push(historyChangeFromChange(doc, change)) - } - } else { - // Can not be merged, start a new event. - var before = lst(hist.done) - if (!before || !before.ranges) - { pushSelectionToHistory(doc.sel, hist.done) } - cur = {changes: [historyChangeFromChange(doc, change)], - generation: hist.generation} - hist.done.push(cur) - while (hist.done.length > hist.undoDepth) { - hist.done.shift() - if (!hist.done[0].ranges) { hist.done.shift() } - } - } - hist.done.push(selAfter) - hist.generation = ++hist.maxGeneration - hist.lastModTime = hist.lastSelTime = time - hist.lastOp = hist.lastSelOp = opId - hist.lastOrigin = hist.lastSelOrigin = change.origin - - if (!last) { signal(doc, "historyAdded") } -} - -function selectionEventCanBeMerged(doc, origin, prev, sel) { - var ch = origin.charAt(0) - return ch == "*" || - ch == "+" && - prev.ranges.length == sel.ranges.length && - prev.somethingSelected() == sel.somethingSelected() && - new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) -} - -// Called whenever the selection changes, sets the new selection as -// the pending selection in the history, and pushes the old pending -// selection into the 'done' array when it was significantly -// different (in number of selected ranges, emptiness, or time). -function addSelectionToHistory(doc, sel, opId, options) { - var hist = doc.history, origin = options && options.origin - - // A new event is started when the previous origin does not match - // the current, or the origins don't allow matching. Origins - // starting with * are always merged, those starting with + are - // merged when similar and close together in time. - if (opId == hist.lastSelOp || - (origin && hist.lastSelOrigin == origin && - (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || - selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) - { hist.done[hist.done.length - 1] = sel } - else - { pushSelectionToHistory(sel, hist.done) } - - hist.lastSelTime = +new Date - hist.lastSelOrigin = origin - hist.lastSelOp = opId - if (options && options.clearRedo !== false) - { clearSelectionEvents(hist.undone) } -} - -function pushSelectionToHistory(sel, dest) { - var top = lst(dest) - if (!(top && top.ranges && top.equals(sel))) - { dest.push(sel) } -} - -// Used to store marked span information in the history. -function attachLocalSpans(doc, change, from, to) { - var existing = change["spans_" + doc.id], n = 0 - doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { - if (line.markedSpans) - { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans } - ++n - }) -} - -// When un/re-doing restores text containing marked spans, those -// that have been explicitly cleared should not be restored. -function removeClearedSpans(spans) { - if (!spans) { return null } - var out - for (var i = 0; i < spans.length; ++i) { - if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i) } } - else if (out) { out.push(spans[i]) } - } - return !out ? spans : out.length ? out : null -} - -// Retrieve and filter the old marked spans stored in a change event. -function getOldSpans(doc, change) { - var found = change["spans_" + doc.id] - if (!found) { return null } - var nw = [] - for (var i = 0; i < change.text.length; ++i) - { nw.push(removeClearedSpans(found[i])) } - return nw -} - -// Used for un/re-doing changes from the history. Combines the -// result of computing the existing spans with the set of spans that -// existed in the history (so that deleting around a span and then -// undoing brings back the span). -function mergeOldSpans(doc, change) { - var old = getOldSpans(doc, change) - var stretched = stretchSpansOverChange(doc, change) - if (!old) { return stretched } - if (!stretched) { return old } - - for (var i = 0; i < old.length; ++i) { - var oldCur = old[i], stretchCur = stretched[i] - if (oldCur && stretchCur) { - spans: for (var j = 0; j < stretchCur.length; ++j) { - var span = stretchCur[j] - for (var k = 0; k < oldCur.length; ++k) - { if (oldCur[k].marker == span.marker) { continue spans } } - oldCur.push(span) - } - } else if (stretchCur) { - old[i] = stretchCur - } - } - return old -} - -// Used both to provide a JSON-safe object in .getHistory, and, when -// detaching a document, to split the history in two -function copyHistoryArray(events, newGroup, instantiateSel) { - var copy = [] - for (var i = 0; i < events.length; ++i) { - var event = events[i] - if (event.ranges) { - copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event) - continue - } - var changes = event.changes, newChanges = [] - copy.push({changes: newChanges}) - for (var j = 0; j < changes.length; ++j) { - var change = changes[j], m = (void 0) - newChanges.push({from: change.from, to: change.to, text: change.text}) - if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { - if (indexOf(newGroup, Number(m[1])) > -1) { - lst(newChanges)[prop] = change[prop] - delete change[prop] - } - } } } - } - } - return copy -} - -// The 'scroll' parameter given to many of these indicated whether -// the new cursor position should be scrolled into view after -// modifying the selection. - -// If shift is held or the extend flag is set, extends a range to -// include a given position (and optionally a second position). -// Otherwise, simply returns the range between the given positions. -// Used for cursor motion and such. -function extendRange(range, head, other, extend) { - if (extend) { - var anchor = range.anchor - if (other) { - var posBefore = cmp(head, anchor) < 0 - if (posBefore != (cmp(other, anchor) < 0)) { - anchor = head - head = other - } else if (posBefore != (cmp(head, other) < 0)) { - head = other - } - } - return new Range(anchor, head) - } else { - return new Range(other || head, head) - } -} - -// Extend the primary selection range, discard the rest. -function extendSelection(doc, head, other, options, extend) { - if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend) } - setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options) -} - -// Extend all selections (pos is an array of selections with length -// equal the number of selections) -function extendSelections(doc, heads, options) { - var out = [] - var extend = doc.cm && (doc.cm.display.shift || doc.extend) - for (var i = 0; i < doc.sel.ranges.length; i++) - { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend) } - var newSel = normalizeSelection(out, doc.sel.primIndex) - setSelection(doc, newSel, options) -} - -// Updates a single range in the selection. -function replaceOneSelection(doc, i, range, options) { - var ranges = doc.sel.ranges.slice(0) - ranges[i] = range - setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options) -} - -// Reset the selection to a single range. -function setSimpleSelection(doc, anchor, head, options) { - setSelection(doc, simpleSelection(anchor, head), options) -} - -// Give beforeSelectionChange handlers a change to influence a -// selection update. -function filterSelectionChange(doc, sel, options) { - var obj = { - ranges: sel.ranges, - update: function(ranges) { - var this$1 = this; - - this.ranges = [] - for (var i = 0; i < ranges.length; i++) - { this$1.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), - clipPos(doc, ranges[i].head)) } - }, - origin: options && options.origin - } - signal(doc, "beforeSelectionChange", doc, obj) - if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj) } - if (obj.ranges != sel.ranges) { return normalizeSelection(obj.ranges, obj.ranges.length - 1) } - else { return sel } -} - -function setSelectionReplaceHistory(doc, sel, options) { - var done = doc.history.done, last = lst(done) - if (last && last.ranges) { - done[done.length - 1] = sel - setSelectionNoUndo(doc, sel, options) - } else { - setSelection(doc, sel, options) - } -} - -// Set a new selection. -function setSelection(doc, sel, options) { - setSelectionNoUndo(doc, sel, options) - addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options) -} - -function setSelectionNoUndo(doc, sel, options) { - if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) - { sel = filterSelectionChange(doc, sel, options) } - - var bias = options && options.bias || - (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1) - setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)) - - if (!(options && options.scroll === false) && doc.cm) - { ensureCursorVisible(doc.cm) } -} - -function setSelectionInner(doc, sel) { - if (sel.equals(doc.sel)) { return } - - doc.sel = sel - - if (doc.cm) { - doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true - signalCursorActivity(doc.cm) - } - signalLater(doc, "cursorActivity", doc) -} - -// Verify that the selection does not partially select any atomic -// marked ranges. -function reCheckSelection(doc) { - setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)) -} - -// Return a selection that does not partially select any atomic -// ranges. -function skipAtomicInSelection(doc, sel, bias, mayClear) { - var out - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i] - var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i] - var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear) - var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear) - if (out || newAnchor != range.anchor || newHead != range.head) { - if (!out) { out = sel.ranges.slice(0, i) } - out[i] = new Range(newAnchor, newHead) - } - } - return out ? normalizeSelection(out, sel.primIndex) : sel -} - -function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { - var line = getLine(doc, pos.line) - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var sp = line.markedSpans[i], m = sp.marker - if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && - (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { - if (mayClear) { - signal(m, "beforeCursorEnter") - if (m.explicitlyCleared) { - if (!line.markedSpans) { break } - else {--i; continue} - } - } - if (!m.atomic) { continue } - - if (oldPos) { - var near = m.find(dir < 0 ? 1 : -1), diff = (void 0) - if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) - { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null) } - if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) - { return skipAtomicInner(doc, near, pos, dir, mayClear) } - } - - var far = m.find(dir < 0 ? -1 : 1) - if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) - { far = movePos(doc, far, dir, far.line == pos.line ? line : null) } - return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null - } - } } - return pos -} - -// Ensure a given position is not inside an atomic range. -function skipAtomic(doc, pos, oldPos, bias, mayClear) { - var dir = bias || 1 - var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || - skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)) - if (!found) { - doc.cantEdit = true - return Pos(doc.first, 0) - } - return found -} - -function movePos(doc, pos, dir, line) { - if (dir < 0 && pos.ch == 0) { - if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } - else { return null } - } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { - if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } - else { return null } - } else { - return new Pos(pos.line, pos.ch + dir) - } -} - -function selectAll(cm) { - cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll) -} - -// UPDATING - -// Allow "beforeChange" event handlers to influence a change -function filterChange(doc, change, update) { - var obj = { - canceled: false, - from: change.from, - to: change.to, - text: change.text, - origin: change.origin, - cancel: function () { return obj.canceled = true; } - } - if (update) { obj.update = function (from, to, text, origin) { - if (from) { obj.from = clipPos(doc, from) } - if (to) { obj.to = clipPos(doc, to) } - if (text) { obj.text = text } - if (origin !== undefined) { obj.origin = origin } - } } - signal(doc, "beforeChange", doc, obj) - if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj) } - - if (obj.canceled) { return null } - return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} -} - -// Apply a change to a document, and add it to the document's -// history, and propagating it to all linked documents. -function makeChange(doc, change, ignoreReadOnly) { - if (doc.cm) { - if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } - if (doc.cm.state.suppressEdits) { return } - } - - if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { - change = filterChange(doc, change, true) - if (!change) { return } - } - - // Possibly split or suppress the update based on the presence - // of read-only spans in its range. - var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to) - if (split) { - for (var i = split.length - 1; i >= 0; --i) - { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}) } - } else { - makeChangeInner(doc, change) - } -} - -function makeChangeInner(doc, change) { - if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } - var selAfter = computeSelAfterChange(doc, change) - addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN) - - makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)) - var rebased = [] - - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change) - rebased.push(doc.history) - } - makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)) - }) -} - -// Revert a change stored in a document's history. -function makeChangeFromHistory(doc, type, allowSelectionOnly) { - if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) { return } - - var hist = doc.history, event, selAfter = doc.sel - var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done - - // Verify that there is a useable event (so that ctrl-z won't - // needlessly clear selection events) - var i = 0 - for (; i < source.length; i++) { - event = source[i] - if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) - { break } - } - if (i == source.length) { return } - hist.lastOrigin = hist.lastSelOrigin = null - - for (;;) { - event = source.pop() - if (event.ranges) { - pushSelectionToHistory(event, dest) - if (allowSelectionOnly && !event.equals(doc.sel)) { - setSelection(doc, event, {clearRedo: false}) - return - } - selAfter = event - } - else { break } - } - - // Build up a reverse change object to add to the opposite history - // stack (redo when undoing, and vice versa). - var antiChanges = [] - pushSelectionToHistory(selAfter, dest) - dest.push({changes: antiChanges, generation: hist.generation}) - hist.generation = event.generation || ++hist.maxGeneration - - var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange") - - var loop = function ( i ) { - var change = event.changes[i] - change.origin = type - if (filter && !filterChange(doc, change, false)) { - source.length = 0 - return {} - } - - antiChanges.push(historyChangeFromChange(doc, change)) - - var after = i ? computeSelAfterChange(doc, change) : lst(source) - makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)) - if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}) } - var rebased = [] - - // Propagate to the linked documents - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change) - rebased.push(doc.history) - } - makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)) - }) - }; - - for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { - var returned = loop( i$1 ); - - if ( returned ) return returned.v; - } -} - -// Sub-views need their line numbers shifted when text is added -// above or below them in the parent document. -function shiftDoc(doc, distance) { - if (distance == 0) { return } - doc.first += distance - doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( - Pos(range.anchor.line + distance, range.anchor.ch), - Pos(range.head.line + distance, range.head.ch) - ); }), doc.sel.primIndex) - if (doc.cm) { - regChange(doc.cm, doc.first, doc.first - distance, distance) - for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) - { regLineChange(doc.cm, l, "gutter") } - } -} - -// More lower-level change function, handling only a single document -// (not linked ones). -function makeChangeSingleDoc(doc, change, selAfter, spans) { - if (doc.cm && !doc.cm.curOp) - { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } - - if (change.to.line < doc.first) { - shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)) - return - } - if (change.from.line > doc.lastLine()) { return } - - // Clip the change to the size of this doc - if (change.from.line < doc.first) { - var shift = change.text.length - 1 - (doc.first - change.from.line) - shiftDoc(doc, shift) - change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), - text: [lst(change.text)], origin: change.origin} - } - var last = doc.lastLine() - if (change.to.line > last) { - change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), - text: [change.text[0]], origin: change.origin} - } - - change.removed = getBetween(doc, change.from, change.to) - - if (!selAfter) { selAfter = computeSelAfterChange(doc, change) } - if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans) } - else { updateDoc(doc, change, spans) } - setSelectionNoUndo(doc, selAfter, sel_dontScroll) -} - -// Handle the interaction of a change to a document with the editor -// that this document is part of. -function makeChangeSingleDocInEditor(cm, change, spans) { - var doc = cm.doc, display = cm.display, from = change.from, to = change.to - - var recomputeMaxLength = false, checkWidthStart = from.line - if (!cm.options.lineWrapping) { - checkWidthStart = lineNo(visualLine(getLine(doc, from.line))) - doc.iter(checkWidthStart, to.line + 1, function (line) { - if (line == display.maxLine) { - recomputeMaxLength = true - return true - } - }) - } - - if (doc.sel.contains(change.from, change.to) > -1) - { signalCursorActivity(cm) } - - updateDoc(doc, change, spans, estimateHeight(cm)) - - if (!cm.options.lineWrapping) { - doc.iter(checkWidthStart, from.line + change.text.length, function (line) { - var len = lineLength(line) - if (len > display.maxLineLength) { - display.maxLine = line - display.maxLineLength = len - display.maxLineChanged = true - recomputeMaxLength = false - } - }) - if (recomputeMaxLength) { cm.curOp.updateMaxLine = true } - } - - retreatFrontier(doc, from.line) - startWorker(cm, 400) - - var lendiff = change.text.length - (to.line - from.line) - 1 - // Remember that these lines changed, for updating the display - if (change.full) - { regChange(cm) } - else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) - { regLineChange(cm, from.line, "text") } - else - { regChange(cm, from.line, to.line + 1, lendiff) } - - var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change") - if (changeHandler || changesHandler) { - var obj = { - from: from, to: to, - text: change.text, - removed: change.removed, - origin: change.origin - } - if (changeHandler) { signalLater(cm, "change", cm, obj) } - if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj) } - } - cm.display.selForContextMenu = null -} - -function replaceRange(doc, code, from, to, origin) { - if (!to) { to = from } - if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp } - if (typeof code == "string") { code = doc.splitLines(code) } - makeChange(doc, {from: from, to: to, text: code, origin: origin}) -} - -// Rebasing/resetting history to deal with externally-sourced changes - -function rebaseHistSelSingle(pos, from, to, diff) { - if (to < pos.line) { - pos.line += diff - } else if (from < pos.line) { - pos.line = from - pos.ch = 0 - } -} - -// Tries to rebase an array of history events given a change in the -// document. If the change touches the same lines as the event, the -// event, and everything 'behind' it, is discarded. If the change is -// before the event, the event's positions are updated. Uses a -// copy-on-write scheme for the positions, to avoid having to -// reallocate them all on every rebase, but also avoid problems with -// shared position objects being unsafely updated. -function rebaseHistArray(array, from, to, diff) { - for (var i = 0; i < array.length; ++i) { - var sub = array[i], ok = true - if (sub.ranges) { - if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true } - for (var j = 0; j < sub.ranges.length; j++) { - rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff) - rebaseHistSelSingle(sub.ranges[j].head, from, to, diff) - } - continue - } - for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { - var cur = sub.changes[j$1] - if (to < cur.from.line) { - cur.from = Pos(cur.from.line + diff, cur.from.ch) - cur.to = Pos(cur.to.line + diff, cur.to.ch) - } else if (from <= cur.to.line) { - ok = false - break - } - } - if (!ok) { - array.splice(0, i + 1) - i = 0 - } - } -} - -function rebaseHist(hist, change) { - var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1 - rebaseHistArray(hist.done, from, to, diff) - rebaseHistArray(hist.undone, from, to, diff) -} - -// Utility for applying a change to a line by handle or number, -// returning the number and optionally registering the line as -// changed. -function changeLine(doc, handle, changeType, op) { - var no = handle, line = handle - if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)) } - else { no = lineNo(handle) } - if (no == null) { return null } - if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType) } - return line -} - -// The document is represented as a BTree consisting of leaves, with -// chunk of lines in them, and branches, with up to ten leaves or -// other branch nodes below them. The top node is always a branch -// node, and is the document object itself (meaning it has -// additional methods and properties). -// -// All nodes have parent links. The tree is used both to go from -// line numbers to line objects, and to go from objects to numbers. -// It also indexes by height, and is used to convert between height -// and line object, and to find the total height of the document. -// -// See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html - -function LeafChunk(lines) { - var this$1 = this; - - this.lines = lines - this.parent = null - var height = 0 - for (var i = 0; i < lines.length; ++i) { - lines[i].parent = this$1 - height += lines[i].height - } - this.height = height -} - -LeafChunk.prototype = { - chunkSize: function chunkSize() { return this.lines.length }, - - // Remove the n lines at offset 'at'. - removeInner: function removeInner(at, n) { - var this$1 = this; - - for (var i = at, e = at + n; i < e; ++i) { - var line = this$1.lines[i] - this$1.height -= line.height - cleanUpLine(line) - signalLater(line, "delete") - } - this.lines.splice(at, n) - }, - - // Helper used to collapse a small branch into a single leaf. - collapse: function collapse(lines) { - lines.push.apply(lines, this.lines) - }, - - // Insert the given array of lines at offset 'at', count them as - // having the given height. - insertInner: function insertInner(at, lines, height) { - var this$1 = this; - - this.height += height - this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)) - for (var i = 0; i < lines.length; ++i) { lines[i].parent = this$1 } - }, - - // Used to iterate over a part of the tree. - iterN: function iterN(at, n, op) { - var this$1 = this; - - for (var e = at + n; at < e; ++at) - { if (op(this$1.lines[at])) { return true } } - } -} - -function BranchChunk(children) { - var this$1 = this; - - this.children = children - var size = 0, height = 0 - for (var i = 0; i < children.length; ++i) { - var ch = children[i] - size += ch.chunkSize(); height += ch.height - ch.parent = this$1 - } - this.size = size - this.height = height - this.parent = null -} - -BranchChunk.prototype = { - chunkSize: function chunkSize() { return this.size }, - - removeInner: function removeInner(at, n) { - var this$1 = this; - - this.size -= n - for (var i = 0; i < this.children.length; ++i) { - var child = this$1.children[i], sz = child.chunkSize() - if (at < sz) { - var rm = Math.min(n, sz - at), oldHeight = child.height - child.removeInner(at, rm) - this$1.height -= oldHeight - child.height - if (sz == rm) { this$1.children.splice(i--, 1); child.parent = null } - if ((n -= rm) == 0) { break } - at = 0 - } else { at -= sz } - } - // If the result is smaller than 25 lines, ensure that it is a - // single leaf node. - if (this.size - n < 25 && - (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { - var lines = [] - this.collapse(lines) - this.children = [new LeafChunk(lines)] - this.children[0].parent = this - } - }, - - collapse: function collapse(lines) { - var this$1 = this; - - for (var i = 0; i < this.children.length; ++i) { this$1.children[i].collapse(lines) } - }, - - insertInner: function insertInner(at, lines, height) { - var this$1 = this; - - this.size += lines.length - this.height += height - for (var i = 0; i < this.children.length; ++i) { - var child = this$1.children[i], sz = child.chunkSize() - if (at <= sz) { - child.insertInner(at, lines, height) - if (child.lines && child.lines.length > 50) { - // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. - // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. - var remaining = child.lines.length % 25 + 25 - for (var pos = remaining; pos < child.lines.length;) { - var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)) - child.height -= leaf.height - this$1.children.splice(++i, 0, leaf) - leaf.parent = this$1 - } - child.lines = child.lines.slice(0, remaining) - this$1.maybeSpill() - } - break - } - at -= sz - } - }, - - // When a node has grown, check whether it should be split. - maybeSpill: function maybeSpill() { - if (this.children.length <= 10) { return } - var me = this - do { - var spilled = me.children.splice(me.children.length - 5, 5) - var sibling = new BranchChunk(spilled) - if (!me.parent) { // Become the parent node - var copy = new BranchChunk(me.children) - copy.parent = me - me.children = [copy, sibling] - me = copy - } else { - me.size -= sibling.size - me.height -= sibling.height - var myIndex = indexOf(me.parent.children, me) - me.parent.children.splice(myIndex + 1, 0, sibling) - } - sibling.parent = me.parent - } while (me.children.length > 10) - me.parent.maybeSpill() - }, - - iterN: function iterN(at, n, op) { - var this$1 = this; - - for (var i = 0; i < this.children.length; ++i) { - var child = this$1.children[i], sz = child.chunkSize() - if (at < sz) { - var used = Math.min(n, sz - at) - if (child.iterN(at, used, op)) { return true } - if ((n -= used) == 0) { break } - at = 0 - } else { at -= sz } - } - } -} - -// Line widgets are block elements displayed above or below a line. - -var LineWidget = function(doc, node, options) { - var this$1 = this; - - if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) - { this$1[opt] = options[opt] } } } - this.doc = doc - this.node = node -}; - -LineWidget.prototype.clear = function () { - var this$1 = this; - - var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line) - if (no == null || !ws) { return } - for (var i = 0; i < ws.length; ++i) { if (ws[i] == this$1) { ws.splice(i--, 1) } } - if (!ws.length) { line.widgets = null } - var height = widgetHeight(this) - updateLineHeight(line, Math.max(0, line.height - height)) - if (cm) { - runInOp(cm, function () { - adjustScrollWhenAboveVisible(cm, line, -height) - regLineChange(cm, no, "widget") - }) - signalLater(cm, "lineWidgetCleared", cm, this, no) - } -}; - -LineWidget.prototype.changed = function () { - var this$1 = this; - - var oldH = this.height, cm = this.doc.cm, line = this.line - this.height = null - var diff = widgetHeight(this) - oldH - if (!diff) { return } - updateLineHeight(line, line.height + diff) - if (cm) { - runInOp(cm, function () { - cm.curOp.forceUpdate = true - adjustScrollWhenAboveVisible(cm, line, diff) - signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)) - }) - } -}; -eventMixin(LineWidget) - -function adjustScrollWhenAboveVisible(cm, line, diff) { - if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) - { addToScrollTop(cm, diff) } -} - -function addLineWidget(doc, handle, node, options) { - var widget = new LineWidget(doc, node, options) - var cm = doc.cm - if (cm && widget.noHScroll) { cm.display.alignWidgets = true } - changeLine(doc, handle, "widget", function (line) { - var widgets = line.widgets || (line.widgets = []) - if (widget.insertAt == null) { widgets.push(widget) } - else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget) } - widget.line = line - if (cm && !lineIsHidden(doc, line)) { - var aboveVisible = heightAtLine(line) < doc.scrollTop - updateLineHeight(line, line.height + widgetHeight(widget)) - if (aboveVisible) { addToScrollTop(cm, widget.height) } - cm.curOp.forceUpdate = true - } - return true - }) - signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)) - return widget -} - -// TEXTMARKERS - -// Created with markText and setBookmark methods. A TextMarker is a -// handle that can be used to clear or find a marked position in the -// document. Line objects hold arrays (markedSpans) containing -// {from, to, marker} object pointing to such marker objects, and -// indicating that such a marker is present on that line. Multiple -// lines may point to the same marker when it spans across lines. -// The spans will have null for their from/to properties when the -// marker continues beyond the start/end of the line. Markers have -// links back to the lines they currently touch. - -// Collapsed markers have unique ids, in order to be able to order -// them, which is needed for uniquely determining an outer marker -// when they overlap (they may nest, but not partially overlap). -var nextMarkerId = 0 - -var TextMarker = function(doc, type) { - this.lines = [] - this.type = type - this.doc = doc - this.id = ++nextMarkerId -}; - -// Clear the marker. -TextMarker.prototype.clear = function () { - var this$1 = this; - - if (this.explicitlyCleared) { return } - var cm = this.doc.cm, withOp = cm && !cm.curOp - if (withOp) { startOperation(cm) } - if (hasHandler(this, "clear")) { - var found = this.find() - if (found) { signalLater(this, "clear", found.from, found.to) } - } - var min = null, max = null - for (var i = 0; i < this.lines.length; ++i) { - var line = this$1.lines[i] - var span = getMarkedSpanFor(line.markedSpans, this$1) - if (cm && !this$1.collapsed) { regLineChange(cm, lineNo(line), "text") } - else if (cm) { - if (span.to != null) { max = lineNo(line) } - if (span.from != null) { min = lineNo(line) } - } - line.markedSpans = removeMarkedSpan(line.markedSpans, span) - if (span.from == null && this$1.collapsed && !lineIsHidden(this$1.doc, line) && cm) - { updateLineHeight(line, textHeight(cm.display)) } - } - if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { - var visual = visualLine(this$1.lines[i$1]), len = lineLength(visual) - if (len > cm.display.maxLineLength) { - cm.display.maxLine = visual - cm.display.maxLineLength = len - cm.display.maxLineChanged = true - } - } } - - if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1) } - this.lines.length = 0 - this.explicitlyCleared = true - if (this.atomic && this.doc.cantEdit) { - this.doc.cantEdit = false - if (cm) { reCheckSelection(cm.doc) } - } - if (cm) { signalLater(cm, "markerCleared", cm, this, min, max) } - if (withOp) { endOperation(cm) } - if (this.parent) { this.parent.clear() } -}; - -// Find the position of the marker in the document. Returns a {from, -// to} object by default. Side can be passed to get a specific side -// -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the -// Pos objects returned contain a line object, rather than a line -// number (used to prevent looking up the same line twice). -TextMarker.prototype.find = function (side, lineObj) { - var this$1 = this; - - if (side == null && this.type == "bookmark") { side = 1 } - var from, to - for (var i = 0; i < this.lines.length; ++i) { - var line = this$1.lines[i] - var span = getMarkedSpanFor(line.markedSpans, this$1) - if (span.from != null) { - from = Pos(lineObj ? line : lineNo(line), span.from) - if (side == -1) { return from } - } - if (span.to != null) { - to = Pos(lineObj ? line : lineNo(line), span.to) - if (side == 1) { return to } - } - } - return from && {from: from, to: to} -}; - -// Signals that the marker's widget changed, and surrounding layout -// should be recomputed. -TextMarker.prototype.changed = function () { - var this$1 = this; - - var pos = this.find(-1, true), widget = this, cm = this.doc.cm - if (!pos || !cm) { return } - runInOp(cm, function () { - var line = pos.line, lineN = lineNo(pos.line) - var view = findViewForLine(cm, lineN) - if (view) { - clearLineMeasurementCacheFor(view) - cm.curOp.selectionChanged = cm.curOp.forceUpdate = true - } - cm.curOp.updateMaxLine = true - if (!lineIsHidden(widget.doc, line) && widget.height != null) { - var oldHeight = widget.height - widget.height = null - var dHeight = widgetHeight(widget) - oldHeight - if (dHeight) - { updateLineHeight(line, line.height + dHeight) } - } - signalLater(cm, "markerChanged", cm, this$1) - }) -}; - -TextMarker.prototype.attachLine = function (line) { - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp - if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) - { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this) } - } - this.lines.push(line) -}; - -TextMarker.prototype.detachLine = function (line) { - this.lines.splice(indexOf(this.lines, line), 1) - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp - ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this) - } -}; -eventMixin(TextMarker) - -// Create a marker, wire it up to the right lines, and -function markText(doc, from, to, options, type) { - // Shared markers (across linked documents) are handled separately - // (markTextShared will call out to this again, once per - // document). - if (options && options.shared) { return markTextShared(doc, from, to, options, type) } - // Ensure we are in an operation. - if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } - - var marker = new TextMarker(doc, type), diff = cmp(from, to) - if (options) { copyObj(options, marker, false) } - // Don't connect empty markers unless clearWhenEmpty is false - if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) - { return marker } - if (marker.replacedWith) { - // Showing up as a widget implies collapsed (widget replaces text) - marker.collapsed = true - marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget") - if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true") } - if (options.insertLeft) { marker.widgetNode.insertLeft = true } - } - if (marker.collapsed) { - if (conflictingCollapsedRange(doc, from.line, from, to, marker) || - from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) - { throw new Error("Inserting collapsed marker partially overlapping an existing one") } - seeCollapsedSpans() - } - - if (marker.addToHistory) - { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN) } - - var curLine = from.line, cm = doc.cm, updateMaxLine - doc.iter(curLine, to.line + 1, function (line) { - if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) - { updateMaxLine = true } - if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0) } - addMarkedSpan(line, new MarkedSpan(marker, - curLine == from.line ? from.ch : null, - curLine == to.line ? to.ch : null)) - ++curLine - }) - // lineIsHidden depends on the presence of the spans, so needs a second pass - if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { - if (lineIsHidden(doc, line)) { updateLineHeight(line, 0) } - }) } - - if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }) } - - if (marker.readOnly) { - seeReadOnlySpans() - if (doc.history.done.length || doc.history.undone.length) - { doc.clearHistory() } - } - if (marker.collapsed) { - marker.id = ++nextMarkerId - marker.atomic = true - } - if (cm) { - // Sync editor state - if (updateMaxLine) { cm.curOp.updateMaxLine = true } - if (marker.collapsed) - { regChange(cm, from.line, to.line + 1) } - else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) - { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text") } } - if (marker.atomic) { reCheckSelection(cm.doc) } - signalLater(cm, "markerAdded", cm, marker) - } - return marker -} - -// SHARED TEXTMARKERS - -// A shared marker spans multiple linked documents. It is -// implemented as a meta-marker-object controlling multiple normal -// markers. -var SharedTextMarker = function(markers, primary) { - var this$1 = this; - - this.markers = markers - this.primary = primary - for (var i = 0; i < markers.length; ++i) - { markers[i].parent = this$1 } -}; - -SharedTextMarker.prototype.clear = function () { - var this$1 = this; - - if (this.explicitlyCleared) { return } - this.explicitlyCleared = true - for (var i = 0; i < this.markers.length; ++i) - { this$1.markers[i].clear() } - signalLater(this, "clear") -}; - -SharedTextMarker.prototype.find = function (side, lineObj) { - return this.primary.find(side, lineObj) -}; -eventMixin(SharedTextMarker) - -function markTextShared(doc, from, to, options, type) { - options = copyObj(options) - options.shared = false - var markers = [markText(doc, from, to, options, type)], primary = markers[0] - var widget = options.widgetNode - linkedDocs(doc, function (doc) { - if (widget) { options.widgetNode = widget.cloneNode(true) } - markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)) - for (var i = 0; i < doc.linked.length; ++i) - { if (doc.linked[i].isParent) { return } } - primary = lst(markers) - }) - return new SharedTextMarker(markers, primary) -} - -function findSharedMarkers(doc) { - return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) -} - -function copySharedMarkers(doc, markers) { - for (var i = 0; i < markers.length; i++) { - var marker = markers[i], pos = marker.find() - var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to) - if (cmp(mFrom, mTo)) { - var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type) - marker.markers.push(subMark) - subMark.parent = marker - } - } -} - -function detachSharedMarkers(markers) { - var loop = function ( i ) { - var marker = markers[i], linked = [marker.primary.doc] - linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }) - for (var j = 0; j < marker.markers.length; j++) { - var subMarker = marker.markers[j] - if (indexOf(linked, subMarker.doc) == -1) { - subMarker.parent = null - marker.markers.splice(j--, 1) - } - } - }; - - for (var i = 0; i < markers.length; i++) loop( i ); -} - -var nextDocId = 0 -var Doc = function(text, mode, firstLine, lineSep, direction) { - if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } - if (firstLine == null) { firstLine = 0 } - - BranchChunk.call(this, [new LeafChunk([new Line("", null)])]) - this.first = firstLine - this.scrollTop = this.scrollLeft = 0 - this.cantEdit = false - this.cleanGeneration = 1 - this.modeFrontier = this.highlightFrontier = firstLine - var start = Pos(firstLine, 0) - this.sel = simpleSelection(start) - this.history = new History(null) - this.id = ++nextDocId - this.modeOption = mode - this.lineSep = lineSep - this.direction = (direction == "rtl") ? "rtl" : "ltr" - this.extend = false - - if (typeof text == "string") { text = this.splitLines(text) } - updateDoc(this, {from: start, to: start, text: text}) - setSelection(this, simpleSelection(start), sel_dontScroll) -} - -Doc.prototype = createObj(BranchChunk.prototype, { - constructor: Doc, - // Iterate over the document. Supports two forms -- with only one - // argument, it calls that for each line in the document. With - // three, it iterates over the range given by the first two (with - // the second being non-inclusive). - iter: function(from, to, op) { - if (op) { this.iterN(from - this.first, to - from, op) } - else { this.iterN(this.first, this.first + this.size, from) } - }, - - // Non-public interface for adding and removing lines. - insert: function(at, lines) { - var height = 0 - for (var i = 0; i < lines.length; ++i) { height += lines[i].height } - this.insertInner(at - this.first, lines, height) - }, - remove: function(at, n) { this.removeInner(at - this.first, n) }, - - // From here, the methods are part of the public interface. Most - // are also available from CodeMirror (editor) instances. - - getValue: function(lineSep) { - var lines = getLines(this, this.first, this.first + this.size) - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - setValue: docMethodOp(function(code) { - var top = Pos(this.first, 0), last = this.first + this.size - 1 - makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), - text: this.splitLines(code), origin: "setValue", full: true}, true) - if (this.cm) { scrollToCoords(this.cm, 0, 0) } - setSelection(this, simpleSelection(top), sel_dontScroll) - }), - replaceRange: function(code, from, to, origin) { - from = clipPos(this, from) - to = to ? clipPos(this, to) : from - replaceRange(this, code, from, to, origin) - }, - getRange: function(from, to, lineSep) { - var lines = getBetween(this, clipPos(this, from), clipPos(this, to)) - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - - getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, - - getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, - getLineNumber: function(line) {return lineNo(line)}, - - getLineHandleVisualStart: function(line) { - if (typeof line == "number") { line = getLine(this, line) } - return visualLine(line) - }, - - lineCount: function() {return this.size}, - firstLine: function() {return this.first}, - lastLine: function() {return this.first + this.size - 1}, - - clipPos: function(pos) {return clipPos(this, pos)}, - - getCursor: function(start) { - var range = this.sel.primary(), pos - if (start == null || start == "head") { pos = range.head } - else if (start == "anchor") { pos = range.anchor } - else if (start == "end" || start == "to" || start === false) { pos = range.to() } - else { pos = range.from() } - return pos - }, - listSelections: function() { return this.sel.ranges }, - somethingSelected: function() {return this.sel.somethingSelected()}, - - setCursor: docMethodOp(function(line, ch, options) { - setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options) - }), - setSelection: docMethodOp(function(anchor, head, options) { - setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options) - }), - extendSelection: docMethodOp(function(head, other, options) { - extendSelection(this, clipPos(this, head), other && clipPos(this, other), options) - }), - extendSelections: docMethodOp(function(heads, options) { - extendSelections(this, clipPosArray(this, heads), options) - }), - extendSelectionsBy: docMethodOp(function(f, options) { - var heads = map(this.sel.ranges, f) - extendSelections(this, clipPosArray(this, heads), options) - }), - setSelections: docMethodOp(function(ranges, primary, options) { - var this$1 = this; - - if (!ranges.length) { return } - var out = [] - for (var i = 0; i < ranges.length; i++) - { out[i] = new Range(clipPos(this$1, ranges[i].anchor), - clipPos(this$1, ranges[i].head)) } - if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex) } - setSelection(this, normalizeSelection(out, primary), options) - }), - addSelection: docMethodOp(function(anchor, head, options) { - var ranges = this.sel.ranges.slice(0) - ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))) - setSelection(this, normalizeSelection(ranges, ranges.length - 1), options) - }), - - getSelection: function(lineSep) { - var this$1 = this; - - var ranges = this.sel.ranges, lines - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this$1, ranges[i].from(), ranges[i].to()) - lines = lines ? lines.concat(sel) : sel - } - if (lineSep === false) { return lines } - else { return lines.join(lineSep || this.lineSeparator()) } - }, - getSelections: function(lineSep) { - var this$1 = this; - - var parts = [], ranges = this.sel.ranges - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this$1, ranges[i].from(), ranges[i].to()) - if (lineSep !== false) { sel = sel.join(lineSep || this$1.lineSeparator()) } - parts[i] = sel - } - return parts - }, - replaceSelection: function(code, collapse, origin) { - var dup = [] - for (var i = 0; i < this.sel.ranges.length; i++) - { dup[i] = code } - this.replaceSelections(dup, collapse, origin || "+input") - }, - replaceSelections: docMethodOp(function(code, collapse, origin) { - var this$1 = this; - - var changes = [], sel = this.sel - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i] - changes[i] = {from: range.from(), to: range.to(), text: this$1.splitLines(code[i]), origin: origin} - } - var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse) - for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) - { makeChange(this$1, changes[i$1]) } - if (newSel) { setSelectionReplaceHistory(this, newSel) } - else if (this.cm) { ensureCursorVisible(this.cm) } - }), - undo: docMethodOp(function() {makeChangeFromHistory(this, "undo")}), - redo: docMethodOp(function() {makeChangeFromHistory(this, "redo")}), - undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true)}), - redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true)}), - - setExtending: function(val) {this.extend = val}, - getExtending: function() {return this.extend}, - - historySize: function() { - var hist = this.history, done = 0, undone = 0 - for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done } } - for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone } } - return {undo: done, redo: undone} - }, - clearHistory: function() {this.history = new History(this.history.maxGeneration)}, - - markClean: function() { - this.cleanGeneration = this.changeGeneration(true) - }, - changeGeneration: function(forceSplit) { - if (forceSplit) - { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null } - return this.history.generation - }, - isClean: function (gen) { - return this.history.generation == (gen || this.cleanGeneration) - }, - - getHistory: function() { - return {done: copyHistoryArray(this.history.done), - undone: copyHistoryArray(this.history.undone)} - }, - setHistory: function(histData) { - var hist = this.history = new History(this.history.maxGeneration) - hist.done = copyHistoryArray(histData.done.slice(0), null, true) - hist.undone = copyHistoryArray(histData.undone.slice(0), null, true) - }, - - setGutterMarker: docMethodOp(function(line, gutterID, value) { - return changeLine(this, line, "gutter", function (line) { - var markers = line.gutterMarkers || (line.gutterMarkers = {}) - markers[gutterID] = value - if (!value && isEmpty(markers)) { line.gutterMarkers = null } - return true - }) - }), - - clearGutter: docMethodOp(function(gutterID) { - var this$1 = this; - - this.iter(function (line) { - if (line.gutterMarkers && line.gutterMarkers[gutterID]) { - changeLine(this$1, line, "gutter", function () { - line.gutterMarkers[gutterID] = null - if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null } - return true - }) - } - }) - }), - - lineInfo: function(line) { - var n - if (typeof line == "number") { - if (!isLine(this, line)) { return null } - n = line - line = getLine(this, line) - if (!line) { return null } - } else { - n = lineNo(line) - if (n == null) { return null } - } - return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, - textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, - widgets: line.widgets} - }, - - addLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass" - if (!line[prop]) { line[prop] = cls } - else if (classTest(cls).test(line[prop])) { return false } - else { line[prop] += " " + cls } - return true - }) - }), - removeLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass" - var cur = line[prop] - if (!cur) { return false } - else if (cls == null) { line[prop] = null } - else { - var found = cur.match(classTest(cls)) - if (!found) { return false } - var end = found.index + found[0].length - line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null - } - return true - }) - }), - - addLineWidget: docMethodOp(function(handle, node, options) { - return addLineWidget(this, handle, node, options) - }), - removeLineWidget: function(widget) { widget.clear() }, - - markText: function(from, to, options) { - return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") - }, - setBookmark: function(pos, options) { - var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), - insertLeft: options && options.insertLeft, - clearWhenEmpty: false, shared: options && options.shared, - handleMouseEvents: options && options.handleMouseEvents} - pos = clipPos(this, pos) - return markText(this, pos, pos, realOpts, "bookmark") - }, - findMarksAt: function(pos) { - pos = clipPos(this, pos) - var markers = [], spans = getLine(this, pos.line).markedSpans - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i] - if ((span.from == null || span.from <= pos.ch) && - (span.to == null || span.to >= pos.ch)) - { markers.push(span.marker.parent || span.marker) } - } } - return markers - }, - findMarks: function(from, to, filter) { - from = clipPos(this, from); to = clipPos(this, to) - var found = [], lineNo = from.line - this.iter(from.line, to.line + 1, function (line) { - var spans = line.markedSpans - if (spans) { for (var i = 0; i < spans.length; i++) { - var span = spans[i] - if (!(span.to != null && lineNo == from.line && from.ch >= span.to || - span.from == null && lineNo != from.line || - span.from != null && lineNo == to.line && span.from >= to.ch) && - (!filter || filter(span.marker))) - { found.push(span.marker.parent || span.marker) } - } } - ++lineNo - }) - return found - }, - getAllMarks: function() { - var markers = [] - this.iter(function (line) { - var sps = line.markedSpans - if (sps) { for (var i = 0; i < sps.length; ++i) - { if (sps[i].from != null) { markers.push(sps[i].marker) } } } - }) - return markers - }, - - posFromIndex: function(off) { - var ch, lineNo = this.first, sepSize = this.lineSeparator().length - this.iter(function (line) { - var sz = line.text.length + sepSize - if (sz > off) { ch = off; return true } - off -= sz - ++lineNo - }) - return clipPos(this, Pos(lineNo, ch)) - }, - indexFromPos: function (coords) { - coords = clipPos(this, coords) - var index = coords.ch - if (coords.line < this.first || coords.ch < 0) { return 0 } - var sepSize = this.lineSeparator().length - this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value - index += line.text.length + sepSize - }) - return index - }, - - copy: function(copyHistory) { - var doc = new Doc(getLines(this, this.first, this.first + this.size), - this.modeOption, this.first, this.lineSep, this.direction) - doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft - doc.sel = this.sel - doc.extend = false - if (copyHistory) { - doc.history.undoDepth = this.history.undoDepth - doc.setHistory(this.getHistory()) - } - return doc - }, - - linkedDoc: function(options) { - if (!options) { options = {} } - var from = this.first, to = this.first + this.size - if (options.from != null && options.from > from) { from = options.from } - if (options.to != null && options.to < to) { to = options.to } - var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction) - if (options.sharedHist) { copy.history = this.history - ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}) - copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}] - copySharedMarkers(copy, findSharedMarkers(this)) - return copy - }, - unlinkDoc: function(other) { - var this$1 = this; - - if (other instanceof CodeMirror) { other = other.doc } - if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { - var link = this$1.linked[i] - if (link.doc != other) { continue } - this$1.linked.splice(i, 1) - other.unlinkDoc(this$1) - detachSharedMarkers(findSharedMarkers(this$1)) - break - } } - // If the histories were shared, split them again - if (other.history == this.history) { - var splitIds = [other.id] - linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true) - other.history = new History(null) - other.history.done = copyHistoryArray(this.history.done, splitIds) - other.history.undone = copyHistoryArray(this.history.undone, splitIds) - } - }, - iterLinkedDocs: function(f) {linkedDocs(this, f)}, - - getMode: function() {return this.mode}, - getEditor: function() {return this.cm}, - - splitLines: function(str) { - if (this.lineSep) { return str.split(this.lineSep) } - return splitLinesAuto(str) - }, - lineSeparator: function() { return this.lineSep || "\n" }, - - setDirection: docMethodOp(function (dir) { - if (dir != "rtl") { dir = "ltr" } - if (dir == this.direction) { return } - this.direction = dir - this.iter(function (line) { return line.order = null; }) - if (this.cm) { directionChanged(this.cm) } - }) -}) - -// Public alias. -Doc.prototype.eachLine = Doc.prototype.iter - -// Kludge to work around strange IE behavior where it'll sometimes -// re-fire a series of drag-related events right after the drop (#1551) -var lastDrop = 0 - -function onDrop(e) { - var cm = this - clearDragCursor(cm) - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) - { return } - e_preventDefault(e) - if (ie) { lastDrop = +new Date } - var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files - if (!pos || cm.isReadOnly()) { return } - // Might be a file drop, in which case we simply extract the text - // and insert it. - if (files && files.length && window.FileReader && window.File) { - var n = files.length, text = Array(n), read = 0 - var loadFile = function (file, i) { - if (cm.options.allowDropFileTypes && - indexOf(cm.options.allowDropFileTypes, file.type) == -1) - { return } - - var reader = new FileReader - reader.onload = operation(cm, function () { - var content = reader.result - if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { content = "" } - text[i] = content - if (++read == n) { - pos = clipPos(cm.doc, pos) - var change = {from: pos, to: pos, - text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())), - origin: "paste"} - makeChange(cm.doc, change) - setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))) - } - }) - reader.readAsText(file) - } - for (var i = 0; i < n; ++i) { loadFile(files[i], i) } - } else { // Normal drop - // Don't do a replace if the drop happened inside of the selected text. - if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { - cm.state.draggingText(e) - // Ensure the editor is re-focused - setTimeout(function () { return cm.display.input.focus(); }, 20) - return - } - try { - var text$1 = e.dataTransfer.getData("Text") - if (text$1) { - var selected - if (cm.state.draggingText && !cm.state.draggingText.copy) - { selected = cm.listSelections() } - setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)) - if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) - { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag") } } - cm.replaceSelection(text$1, "around", "paste") - cm.display.input.focus() - } - } - catch(e){} - } -} - -function onDragStart(cm, e) { - if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } - - e.dataTransfer.setData("Text", cm.getSelection()) - e.dataTransfer.effectAllowed = "copyMove" - - // Use dummy image instead of default browsers image. - // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. - if (e.dataTransfer.setDragImage && !safari) { - var img = elt("img", null, null, "position: fixed; left: 0; top: 0;") - img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" - if (presto) { - img.width = img.height = 1 - cm.display.wrapper.appendChild(img) - // Force a relayout, or Opera won't use our image for some obscure reason - img._top = img.offsetTop - } - e.dataTransfer.setDragImage(img, 0, 0) - if (presto) { img.parentNode.removeChild(img) } - } -} - -function onDragOver(cm, e) { - var pos = posFromMouse(cm, e) - if (!pos) { return } - var frag = document.createDocumentFragment() - drawSelectionCursor(cm, pos, frag) - if (!cm.display.dragCursor) { - cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors") - cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv) - } - removeChildrenAndAdd(cm.display.dragCursor, frag) -} - -function clearDragCursor(cm) { - if (cm.display.dragCursor) { - cm.display.lineSpace.removeChild(cm.display.dragCursor) - cm.display.dragCursor = null - } -} - -// These must be handled carefully, because naively registering a -// handler for each editor will cause the editors to never be -// garbage collected. - -function forEachCodeMirror(f) { - if (!document.getElementsByClassName) { return } - var byClass = document.getElementsByClassName("CodeMirror") - for (var i = 0; i < byClass.length; i++) { - var cm = byClass[i].CodeMirror - if (cm) { f(cm) } - } -} - -var globalsRegistered = false -function ensureGlobalHandlers() { - if (globalsRegistered) { return } - registerGlobalHandlers() - globalsRegistered = true -} -function registerGlobalHandlers() { - // When the window resizes, we need to refresh active editors. - var resizeTimer - on(window, "resize", function () { - if (resizeTimer == null) { resizeTimer = setTimeout(function () { - resizeTimer = null - forEachCodeMirror(onResize) - }, 100) } - }) - // When the window loses focus, we want to show the editor as blurred - on(window, "blur", function () { return forEachCodeMirror(onBlur); }) -} -// Called when the window resizes -function onResize(cm) { - var d = cm.display - if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) - { return } - // Might be a text scaling operation, clear size caches. - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null - d.scrollbarsClipped = false - cm.setSize() -} - -var keyNames = { - 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", - 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", - 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", - 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", - 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", - 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", - 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", - 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" -} - -// Number keys -for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i) } -// Alphabetic keys -for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1) } -// Function keys -for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2 } - -var keyMap = {} - -keyMap.basic = { - "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", - "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", - "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", - "Tab": "defaultTab", "Shift-Tab": "indentAuto", - "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", - "Esc": "singleSelection" -} -// Note that the save and find-related commands aren't defined by -// default. User code or addons can define them. Unknown commands -// are simply ignored. -keyMap.pcDefault = { - "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", - "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", - "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", - "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", - "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", - "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", - "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", - fallthrough: "basic" -} -// Very basic readline/emacs-style bindings, which are standard on Mac. -keyMap.emacsy = { - "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", - "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", - "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", - "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", - "Ctrl-O": "openLine" -} -keyMap.macDefault = { - "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", - "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", - "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", - "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", - "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", - "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", - fallthrough: ["basic", "emacsy"] -} -keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault - -// KEYMAP DISPATCH - -function normalizeKeyName(name) { - var parts = name.split(/-(?!$)/) - name = parts[parts.length - 1] - var alt, ctrl, shift, cmd - for (var i = 0; i < parts.length - 1; i++) { - var mod = parts[i] - if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true } - else if (/^a(lt)?$/i.test(mod)) { alt = true } - else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true } - else if (/^s(hift)?$/i.test(mod)) { shift = true } - else { throw new Error("Unrecognized modifier name: " + mod) } - } - if (alt) { name = "Alt-" + name } - if (ctrl) { name = "Ctrl-" + name } - if (cmd) { name = "Cmd-" + name } - if (shift) { name = "Shift-" + name } - return name -} - -// This is a kludge to keep keymaps mostly working as raw objects -// (backwards compatibility) while at the same time support features -// like normalization and multi-stroke key bindings. It compiles a -// new normalized keymap, and then updates the old object to reflect -// this. -function normalizeKeyMap(keymap) { - var copy = {} - for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { - var value = keymap[keyname] - if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } - if (value == "...") { delete keymap[keyname]; continue } - - var keys = map(keyname.split(" "), normalizeKeyName) - for (var i = 0; i < keys.length; i++) { - var val = (void 0), name = (void 0) - if (i == keys.length - 1) { - name = keys.join(" ") - val = value - } else { - name = keys.slice(0, i + 1).join(" ") - val = "..." - } - var prev = copy[name] - if (!prev) { copy[name] = val } - else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } - } - delete keymap[keyname] - } } - for (var prop in copy) { keymap[prop] = copy[prop] } - return keymap -} - -function lookupKey(key, map, handle, context) { - map = getKeyMap(map) - var found = map.call ? map.call(key, context) : map[key] - if (found === false) { return "nothing" } - if (found === "...") { return "multi" } - if (found != null && handle(found)) { return "handled" } - - if (map.fallthrough) { - if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") - { return lookupKey(key, map.fallthrough, handle, context) } - for (var i = 0; i < map.fallthrough.length; i++) { - var result = lookupKey(key, map.fallthrough[i], handle, context) - if (result) { return result } - } - } -} - -// Modifier key presses don't count as 'real' key presses for the -// purpose of keymap fallthrough. -function isModifierKey(value) { - var name = typeof value == "string" ? value : keyNames[value.keyCode] - return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" -} - -function addModifierNames(name, event, noShift) { - var base = name - if (event.altKey && base != "Alt") { name = "Alt-" + name } - if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name } - if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name } - if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name } - return name -} - -// Look up the name of a key as indicated by an event object. -function keyName(event, noShift) { - if (presto && event.keyCode == 34 && event["char"]) { return false } - var name = keyNames[event.keyCode] - if (name == null || event.altGraphKey) { return false } - return addModifierNames(name, event, noShift) -} - -function getKeyMap(val) { - return typeof val == "string" ? keyMap[val] : val -} - -// Helper for deleting text near the selection(s), used to implement -// backspace, delete, and similar functionality. -function deleteNearSelection(cm, compute) { - var ranges = cm.doc.sel.ranges, kill = [] - // Build up a set of ranges to kill first, merging overlapping - // ranges. - for (var i = 0; i < ranges.length; i++) { - var toKill = compute(ranges[i]) - while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { - var replaced = kill.pop() - if (cmp(replaced.from, toKill.from) < 0) { - toKill.from = replaced.from - break - } - } - kill.push(toKill) - } - // Next, remove those actual ranges. - runInOp(cm, function () { - for (var i = kill.length - 1; i >= 0; i--) - { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete") } - ensureCursorVisible(cm) - }) -} - -// Commands are parameter-less actions that can be performed on an -// editor, mostly used for keybindings. -var commands = { - selectAll: selectAll, - singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, - killLine: function (cm) { return deleteNearSelection(cm, function (range) { - if (range.empty()) { - var len = getLine(cm.doc, range.head.line).text.length - if (range.head.ch == len && range.head.line < cm.lastLine()) - { return {from: range.head, to: Pos(range.head.line + 1, 0)} } - else - { return {from: range.head, to: Pos(range.head.line, len)} } - } else { - return {from: range.from(), to: range.to()} - } - }); }, - deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), - to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) - }); }); }, - delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), to: range.from() - }); }); }, - delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5 - var leftPos = cm.coordsChar({left: 0, top: top}, "div") - return {from: leftPos, to: range.from()} - }); }, - delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5 - var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") - return {from: range.from(), to: rightPos } - }); }, - undo: function (cm) { return cm.undo(); }, - redo: function (cm) { return cm.redo(); }, - undoSelection: function (cm) { return cm.undoSelection(); }, - redoSelection: function (cm) { return cm.redoSelection(); }, - goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, - goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, - goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, - {origin: "+move", bias: 1} - ); }, - goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, - {origin: "+move", bias: 1} - ); }, - goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, - {origin: "+move", bias: -1} - ); }, - goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5 - return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") - }, sel_move); }, - goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5 - return cm.coordsChar({left: 0, top: top}, "div") - }, sel_move); }, - goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5 - var pos = cm.coordsChar({left: 0, top: top}, "div") - if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } - return pos - }, sel_move); }, - goLineUp: function (cm) { return cm.moveV(-1, "line"); }, - goLineDown: function (cm) { return cm.moveV(1, "line"); }, - goPageUp: function (cm) { return cm.moveV(-1, "page"); }, - goPageDown: function (cm) { return cm.moveV(1, "page"); }, - goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, - goCharRight: function (cm) { return cm.moveH(1, "char"); }, - goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, - goColumnRight: function (cm) { return cm.moveH(1, "column"); }, - goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, - goGroupRight: function (cm) { return cm.moveH(1, "group"); }, - goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, - goWordRight: function (cm) { return cm.moveH(1, "word"); }, - delCharBefore: function (cm) { return cm.deleteH(-1, "char"); }, - delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, - delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, - delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, - delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, - delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, - indentAuto: function (cm) { return cm.indentSelection("smart"); }, - indentMore: function (cm) { return cm.indentSelection("add"); }, - indentLess: function (cm) { return cm.indentSelection("subtract"); }, - insertTab: function (cm) { return cm.replaceSelection("\t"); }, - insertSoftTab: function (cm) { - var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize - for (var i = 0; i < ranges.length; i++) { - var pos = ranges[i].from() - var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize) - spaces.push(spaceStr(tabSize - col % tabSize)) - } - cm.replaceSelections(spaces) - }, - defaultTab: function (cm) { - if (cm.somethingSelected()) { cm.indentSelection("add") } - else { cm.execCommand("insertTab") } - }, - // Swap the two chars left and right of each selection's head. - // Move cursor behind the two swapped characters afterwards. - // - // Doesn't consider line feeds a character. - // Doesn't scan more than one line above to find a character. - // Doesn't do anything on an empty line. - // Doesn't do anything with non-empty selections. - transposeChars: function (cm) { return runInOp(cm, function () { - var ranges = cm.listSelections(), newSel = [] - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) { continue } - var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text - if (line) { - if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1) } - if (cur.ch > 0) { - cur = new Pos(cur.line, cur.ch + 1) - cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), - Pos(cur.line, cur.ch - 2), cur, "+transpose") - } else if (cur.line > cm.doc.first) { - var prev = getLine(cm.doc, cur.line - 1).text - if (prev) { - cur = new Pos(cur.line, 1) - cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + - prev.charAt(prev.length - 1), - Pos(cur.line - 1, prev.length - 1), cur, "+transpose") - } - } - } - newSel.push(new Range(cur, cur)) - } - cm.setSelections(newSel) - }); }, - newlineAndIndent: function (cm) { return runInOp(cm, function () { - var sels = cm.listSelections() - for (var i = sels.length - 1; i >= 0; i--) - { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input") } - sels = cm.listSelections() - for (var i$1 = 0; i$1 < sels.length; i$1++) - { cm.indentLine(sels[i$1].from().line, null, true) } - ensureCursorVisible(cm) - }); }, - openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, - toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } -} - - -function lineStart(cm, lineN) { - var line = getLine(cm.doc, lineN) - var visual = visualLine(line) - if (visual != line) { lineN = lineNo(visual) } - return endOfLine(true, cm, visual, lineN, 1) -} -function lineEnd(cm, lineN) { - var line = getLine(cm.doc, lineN) - var visual = visualLineEnd(line) - if (visual != line) { lineN = lineNo(visual) } - return endOfLine(true, cm, line, lineN, -1) -} -function lineStartSmart(cm, pos) { - var start = lineStart(cm, pos.line) - var line = getLine(cm.doc, start.line) - var order = getOrder(line, cm.doc.direction) - if (!order || order[0].level == 0) { - var firstNonWS = Math.max(0, line.text.search(/\S/)) - var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch - return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) - } - return start -} - -// Run a handler that was bound to a key. -function doHandleBinding(cm, bound, dropShift) { - if (typeof bound == "string") { - bound = commands[bound] - if (!bound) { return false } - } - // Ensure previous input has been read, so that the handler sees a - // consistent view of the document - cm.display.input.ensurePolled() - var prevShift = cm.display.shift, done = false - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true } - if (dropShift) { cm.display.shift = false } - done = bound(cm) != Pass - } finally { - cm.display.shift = prevShift - cm.state.suppressEdits = false - } - return done -} - -function lookupKeyForEditor(cm, name, handle) { - for (var i = 0; i < cm.state.keyMaps.length; i++) { - var result = lookupKey(name, cm.state.keyMaps[i], handle, cm) - if (result) { return result } - } - return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) - || lookupKey(name, cm.options.keyMap, handle, cm) -} - -// Note that, despite the name, this function is also used to check -// for bound mouse clicks. - -var stopSeq = new Delayed -function dispatchKey(cm, name, e, handle) { - var seq = cm.state.keySeq - if (seq) { - if (isModifierKey(name)) { return "handled" } - stopSeq.set(50, function () { - if (cm.state.keySeq == seq) { - cm.state.keySeq = null - cm.display.input.reset() - } - }) - name = seq + " " + name - } - var result = lookupKeyForEditor(cm, name, handle) - - if (result == "multi") - { cm.state.keySeq = name } - if (result == "handled") - { signalLater(cm, "keyHandled", cm, name, e) } - - if (result == "handled" || result == "multi") { - e_preventDefault(e) - restartBlink(cm) - } - - if (seq && !result && /\'$/.test(name)) { - e_preventDefault(e) - return true - } - return !!result -} - -// Handle a key from the keydown event. -function handleKeyBinding(cm, e) { - var name = keyName(e, true) - if (!name) { return false } - - if (e.shiftKey && !cm.state.keySeq) { - // First try to resolve full name (including 'Shift-'). Failing - // that, see if there is a cursor-motion command (starting with - // 'go') bound to the keyname without 'Shift-'. - return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) - || dispatchKey(cm, name, e, function (b) { - if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) - { return doHandleBinding(cm, b) } - }) - } else { - return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) - } -} - -// Handle a key from the keypress event -function handleCharBinding(cm, e, ch) { - return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) -} - -var lastStoppedKey = null -function onKeyDown(e) { - var cm = this - cm.curOp.focus = activeElt() - if (signalDOMEvent(cm, e)) { return } - // IE does strange things with escape. - if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false } - var code = e.keyCode - cm.display.shift = code == 16 || e.shiftKey - var handled = handleKeyBinding(cm, e) - if (presto) { - lastStoppedKey = handled ? code : null - // Opera has no cut event... we try to at least catch the key combo - if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) - { cm.replaceSelection("", null, "cut") } - } - - // Turn mouse into crosshair when Alt is held on Mac. - if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) - { showCrossHair(cm) } -} - -function showCrossHair(cm) { - var lineDiv = cm.display.lineDiv - addClass(lineDiv, "CodeMirror-crosshair") - - function up(e) { - if (e.keyCode == 18 || !e.altKey) { - rmClass(lineDiv, "CodeMirror-crosshair") - off(document, "keyup", up) - off(document, "mouseover", up) - } - } - on(document, "keyup", up) - on(document, "mouseover", up) -} - -function onKeyUp(e) { - if (e.keyCode == 16) { this.doc.sel.shift = false } - signalDOMEvent(this, e) -} - -function onKeyPress(e) { - var cm = this - if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } - var keyCode = e.keyCode, charCode = e.charCode - if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} - if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } - var ch = String.fromCharCode(charCode == null ? keyCode : charCode) - // Some browsers fire keypress events for backspace - if (ch == "\x08") { return } - if (handleCharBinding(cm, e, ch)) { return } - cm.display.input.onKeyPress(e) -} - -var DOUBLECLICK_DELAY = 400 - -var PastClick = function(time, pos, button) { - this.time = time - this.pos = pos - this.button = button -}; - -PastClick.prototype.compare = function (time, pos, button) { - return this.time + DOUBLECLICK_DELAY > time && - cmp(pos, this.pos) == 0 && button == this.button -}; - -var lastClick; -var lastDoubleClick; -function clickRepeat(pos, button) { - var now = +new Date - if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { - lastClick = lastDoubleClick = null - return "triple" - } else if (lastClick && lastClick.compare(now, pos, button)) { - lastDoubleClick = new PastClick(now, pos, button) - lastClick = null - return "double" - } else { - lastClick = new PastClick(now, pos, button) - lastDoubleClick = null - return "single" - } -} - -// A mouse down can be a single click, double click, triple click, -// start of selection drag, start of text drag, new cursor -// (ctrl-click), rectangle drag (alt-drag), or xwin -// middle-click-paste. Or it might be a click on something we should -// not interfere with, such as a scrollbar or widget. -function onMouseDown(e) { - var cm = this, display = cm.display - if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } - display.input.ensurePolled() - display.shift = e.shiftKey - - if (eventInWidget(display, e)) { - if (!webkit) { - // Briefly turn off draggability, to allow widgets to do - // normal dragging things. - display.scroller.draggable = false - setTimeout(function () { return display.scroller.draggable = true; }, 100) - } - return - } - if (clickInGutter(cm, e)) { return } - var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single" - window.focus() - - // #3261: make sure, that we're not starting a second selection - if (button == 1 && cm.state.selectingText) - { cm.state.selectingText(e) } - - if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } - - if (button == 1) { - if (pos) { leftButtonDown(cm, pos, repeat, e) } - else if (e_target(e) == display.scroller) { e_preventDefault(e) } - } else if (button == 2) { - if (pos) { extendSelection(cm.doc, pos) } - setTimeout(function () { return display.input.focus(); }, 20) - } else if (button == 3) { - if (captureRightClick) { onContextMenu(cm, e) } - else { delayBlurEvent(cm) } - } -} - -function handleMappedButton(cm, button, pos, repeat, event) { - var name = "Click" - if (repeat == "double") { name = "Double" + name } - else if (repeat == "triple") { name = "Triple" + name } - name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name - - return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { - if (typeof bound == "string") { bound = commands[bound] } - if (!bound) { return false } - var done = false - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true } - done = bound(cm, pos) != Pass - } finally { - cm.state.suppressEdits = false - } - return done - }) -} - -function configureMouse(cm, repeat, event) { - var option = cm.getOption("configureMouse") - var value = option ? option(cm, repeat, event) : {} - if (value.unit == null) { - var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey - value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line" - } - if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey } - if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey } - if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey) } - return value -} - -function leftButtonDown(cm, pos, repeat, event) { - if (ie) { setTimeout(bind(ensureFocus, cm), 0) } - else { cm.curOp.focus = activeElt() } - - var behavior = configureMouse(cm, repeat, event) - - var sel = cm.doc.sel, contained - if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && - repeat == "single" && (contained = sel.contains(pos)) > -1 && - (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && - (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) - { leftButtonStartDrag(cm, event, pos, behavior) } - else - { leftButtonSelect(cm, event, pos, behavior) } -} - -// Start a text drag. When it ends, see if any dragging actually -// happen, and treat as a click if it didn't. -function leftButtonStartDrag(cm, event, pos, behavior) { - var display = cm.display, moved = false - var dragEnd = operation(cm, function (e) { - if (webkit) { display.scroller.draggable = false } - cm.state.draggingText = false - off(document, "mouseup", dragEnd) - off(document, "mousemove", mouseMove) - off(display.scroller, "dragstart", dragStart) - off(display.scroller, "drop", dragEnd) - if (!moved) { - e_preventDefault(e) - if (!behavior.addNew) - { extendSelection(cm.doc, pos, null, null, behavior.extend) } - // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) - if (webkit || ie && ie_version == 9) - { setTimeout(function () {document.body.focus(); display.input.focus()}, 20) } - else - { display.input.focus() } - } - }) - var mouseMove = function(e2) { - moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10 - } - var dragStart = function () { return moved = true; } - // Let the drag handler handle this. - if (webkit) { display.scroller.draggable = true } - cm.state.draggingText = dragEnd - dragEnd.copy = !behavior.moveOnDrag - // IE's approach to draggable - if (display.scroller.dragDrop) { display.scroller.dragDrop() } - on(document, "mouseup", dragEnd) - on(document, "mousemove", mouseMove) - on(display.scroller, "dragstart", dragStart) - on(display.scroller, "drop", dragEnd) - - delayBlurEvent(cm) - setTimeout(function () { return display.input.focus(); }, 20) -} - -function rangeForUnit(cm, pos, unit) { - if (unit == "char") { return new Range(pos, pos) } - if (unit == "word") { return cm.findWordAt(pos) } - if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } - var result = unit(cm, pos) - return new Range(result.from, result.to) -} - -// Normal selection, as opposed to text dragging. -function leftButtonSelect(cm, event, start, behavior) { - var display = cm.display, doc = cm.doc - e_preventDefault(event) - - var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges - if (behavior.addNew && !behavior.extend) { - ourIndex = doc.sel.contains(start) - if (ourIndex > -1) - { ourRange = ranges[ourIndex] } - else - { ourRange = new Range(start, start) } - } else { - ourRange = doc.sel.primary() - ourIndex = doc.sel.primIndex - } - - if (behavior.unit == "rectangle") { - if (!behavior.addNew) { ourRange = new Range(start, start) } - start = posFromMouse(cm, event, true, true) - ourIndex = -1 - } else { - var range = rangeForUnit(cm, start, behavior.unit) - if (behavior.extend) - { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend) } - else - { ourRange = range } - } - - if (!behavior.addNew) { - ourIndex = 0 - setSelection(doc, new Selection([ourRange], 0), sel_mouse) - startSel = doc.sel - } else if (ourIndex == -1) { - ourIndex = ranges.length - setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), - {scroll: false, origin: "*mouse"}) - } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { - setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), - {scroll: false, origin: "*mouse"}) - startSel = doc.sel - } else { - replaceOneSelection(doc, ourIndex, ourRange, sel_mouse) - } - - var lastPos = start - function extendTo(pos) { - if (cmp(lastPos, pos) == 0) { return } - lastPos = pos - - if (behavior.unit == "rectangle") { - var ranges = [], tabSize = cm.options.tabSize - var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize) - var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize) - var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol) - for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); - line <= end; line++) { - var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize) - if (left == right) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))) } - else if (text.length > leftPos) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))) } - } - if (!ranges.length) { ranges.push(new Range(start, start)) } - setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), - {origin: "*mouse", scroll: false}) - cm.scrollIntoView(pos) - } else { - var oldRange = ourRange - var range = rangeForUnit(cm, pos, behavior.unit) - var anchor = oldRange.anchor, head - if (cmp(range.anchor, anchor) > 0) { - head = range.head - anchor = minPos(oldRange.from(), range.anchor) - } else { - head = range.anchor - anchor = maxPos(oldRange.to(), range.head) - } - var ranges$1 = startSel.ranges.slice(0) - ranges$1[ourIndex] = new Range(clipPos(doc, anchor), head) - setSelection(doc, normalizeSelection(ranges$1, ourIndex), sel_mouse) - } - } - - var editorSize = display.wrapper.getBoundingClientRect() - // Used to ensure timeout re-tries don't fire when another extend - // happened in the meantime (clearTimeout isn't reliable -- at - // least on Chrome, the timeouts still happen even when cleared, - // if the clear happens after their scheduled firing time). - var counter = 0 - - function extend(e) { - var curCount = ++counter - var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle") - if (!cur) { return } - if (cmp(cur, lastPos) != 0) { - cm.curOp.focus = activeElt() - extendTo(cur) - var visible = visibleLines(display, doc) - if (cur.line >= visible.to || cur.line < visible.from) - { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e) }}), 150) } - } else { - var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0 - if (outside) { setTimeout(operation(cm, function () { - if (counter != curCount) { return } - display.scroller.scrollTop += outside - extend(e) - }), 50) } - } - } - - function done(e) { - cm.state.selectingText = false - counter = Infinity - e_preventDefault(e) - display.input.focus() - off(document, "mousemove", move) - off(document, "mouseup", up) - doc.history.lastSelOrigin = null - } - - var move = operation(cm, function (e) { - if (!e_button(e)) { done(e) } - else { extend(e) } - }) - var up = operation(cm, done) - cm.state.selectingText = up - on(document, "mousemove", move) - on(document, "mouseup", up) -} - - -// Determines whether an event happened in the gutter, and fires the -// handlers for the corresponding event. -function gutterEvent(cm, e, type, prevent) { - var mX, mY - try { mX = e.clientX; mY = e.clientY } - catch(e) { return false } - if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } - if (prevent) { e_preventDefault(e) } - - var display = cm.display - var lineBox = display.lineDiv.getBoundingClientRect() - - if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } - mY -= lineBox.top - display.viewOffset - - for (var i = 0; i < cm.options.gutters.length; ++i) { - var g = display.gutters.childNodes[i] - if (g && g.getBoundingClientRect().right >= mX) { - var line = lineAtHeight(cm.doc, mY) - var gutter = cm.options.gutters[i] - signal(cm, type, cm, line, gutter, e) - return e_defaultPrevented(e) - } - } -} - -function clickInGutter(cm, e) { - return gutterEvent(cm, e, "gutterClick", true) -} - -// CONTEXT MENU HANDLING - -// To make the context menu work, we need to briefly unhide the -// textarea (making it as unobtrusive as possible) to let the -// right-click take effect on it. -function onContextMenu(cm, e) { - if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } - if (signalDOMEvent(cm, e, "contextmenu")) { return } - cm.display.input.onContextMenu(e) -} - -function contextMenuInGutter(cm, e) { - if (!hasHandler(cm, "gutterContextMenu")) { return false } - return gutterEvent(cm, e, "gutterContextMenu", false) -} - -function themeChanged(cm) { - cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + - cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-") - clearCaches(cm) -} - -var Init = {toString: function(){return "CodeMirror.Init"}} - -var defaults = {} -var optionHandlers = {} - -function defineOptions(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers - - function option(name, deflt, handle, notOnInit) { - CodeMirror.defaults[name] = deflt - if (handle) { optionHandlers[name] = - notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old) }} : handle } - } - - CodeMirror.defineOption = option - - // Passed to option handlers when there is no old value. - CodeMirror.Init = Init - - // These two are, on init, called from the constructor because they - // have to be initialized before the editor can start at all. - option("value", "", function (cm, val) { return cm.setValue(val); }, true) - option("mode", null, function (cm, val) { - cm.doc.modeOption = val - loadMode(cm) - }, true) - - option("indentUnit", 2, loadMode, true) - option("indentWithTabs", false) - option("smartIndent", true) - option("tabSize", 4, function (cm) { - resetModeState(cm) - clearCaches(cm) - regChange(cm) - }, true) - option("lineSeparator", null, function (cm, val) { - cm.doc.lineSep = val - if (!val) { return } - var newBreaks = [], lineNo = cm.doc.first - cm.doc.iter(function (line) { - for (var pos = 0;;) { - var found = line.text.indexOf(val, pos) - if (found == -1) { break } - pos = found + val.length - newBreaks.push(Pos(lineNo, found)) - } - lineNo++ - }) - for (var i = newBreaks.length - 1; i >= 0; i--) - { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) } - }) - option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) { - cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g") - if (old != Init) { cm.refresh() } - }) - option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true) - option("electricChars", true) - option("inputStyle", mobile ? "contenteditable" : "textarea", function () { - throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME - }, true) - option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true) - option("rtlMoveVisually", !windows) - option("wholeLineUpdateBefore", true) - - option("theme", "default", function (cm) { - themeChanged(cm) - guttersChanged(cm) - }, true) - option("keyMap", "default", function (cm, val, old) { - var next = getKeyMap(val) - var prev = old != Init && getKeyMap(old) - if (prev && prev.detach) { prev.detach(cm, next) } - if (next.attach) { next.attach(cm, prev || null) } - }) - option("extraKeys", null) - option("configureMouse", null) - - option("lineWrapping", false, wrappingChanged, true) - option("gutters", [], function (cm) { - setGuttersForLineNumbers(cm.options) - guttersChanged(cm) - }, true) - option("fixedGutter", true, function (cm, val) { - cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0" - cm.refresh() - }, true) - option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true) - option("scrollbarStyle", "native", function (cm) { - initScrollbars(cm) - updateScrollbars(cm) - cm.display.scrollbars.setScrollTop(cm.doc.scrollTop) - cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft) - }, true) - option("lineNumbers", false, function (cm) { - setGuttersForLineNumbers(cm.options) - guttersChanged(cm) - }, true) - option("firstLineNumber", 1, guttersChanged, true) - option("lineNumberFormatter", function (integer) { return integer; }, guttersChanged, true) - option("showCursorWhenSelecting", false, updateSelection, true) - - option("resetSelectionOnContextMenu", true) - option("lineWiseCopyCut", true) - option("pasteLinesPerSelection", true) - - option("readOnly", false, function (cm, val) { - if (val == "nocursor") { - onBlur(cm) - cm.display.input.blur() - } - cm.display.input.readOnlyChanged(val) - }) - option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset() }}, true) - option("dragDrop", true, dragDropChanged) - option("allowDropFileTypes", null) - - option("cursorBlinkRate", 530) - option("cursorScrollMargin", 0) - option("cursorHeight", 1, updateSelection, true) - option("singleCursorHeightPerLine", true, updateSelection, true) - option("workTime", 100) - option("workDelay", 100) - option("flattenSpans", true, resetModeState, true) - option("addModeClass", false, resetModeState, true) - option("pollInterval", 100) - option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }) - option("historyEventDelay", 1250) - option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true) - option("maxHighlightLength", 10000, resetModeState, true) - option("moveInputWithCursor", true, function (cm, val) { - if (!val) { cm.display.input.resetPosition() } - }) - - option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }) - option("autofocus", null) - option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true) -} - -function guttersChanged(cm) { - updateGutters(cm) - regChange(cm) - alignHorizontally(cm) -} - -function dragDropChanged(cm, value, old) { - var wasOn = old && old != Init - if (!value != !wasOn) { - var funcs = cm.display.dragFunctions - var toggle = value ? on : off - toggle(cm.display.scroller, "dragstart", funcs.start) - toggle(cm.display.scroller, "dragenter", funcs.enter) - toggle(cm.display.scroller, "dragover", funcs.over) - toggle(cm.display.scroller, "dragleave", funcs.leave) - toggle(cm.display.scroller, "drop", funcs.drop) - } -} - -function wrappingChanged(cm) { - if (cm.options.lineWrapping) { - addClass(cm.display.wrapper, "CodeMirror-wrap") - cm.display.sizer.style.minWidth = "" - cm.display.sizerWidth = null - } else { - rmClass(cm.display.wrapper, "CodeMirror-wrap") - findMaxLine(cm) - } - estimateLineHeights(cm) - regChange(cm) - clearCaches(cm) - setTimeout(function () { return updateScrollbars(cm); }, 100) -} - -// A CodeMirror instance represents an editor. This is the object -// that user code is usually dealing with. - -function CodeMirror(place, options) { - var this$1 = this; - - if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } - - this.options = options = options ? copyObj(options) : {} - // Determine effective options based on given values and defaults. - copyObj(defaults, options, false) - setGuttersForLineNumbers(options) - - var doc = options.value - if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) } - this.doc = doc - - var input = new CodeMirror.inputStyles[options.inputStyle](this) - var display = this.display = new Display(place, doc, input) - display.wrapper.CodeMirror = this - updateGutters(this) - themeChanged(this) - if (options.lineWrapping) - { this.display.wrapper.className += " CodeMirror-wrap" } - initScrollbars(this) - - this.state = { - keyMaps: [], // stores maps added by addKeyMap - overlays: [], // highlighting overlays, as added by addOverlay - modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info - overwrite: false, - delayingBlurEvent: false, - focused: false, - suppressEdits: false, // used to disable editing during key handlers when in readOnly mode - pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll - selectingText: false, - draggingText: false, - highlight: new Delayed(), // stores highlight worker timeout - keySeq: null, // Unfinished key sequence - specialChars: null - } - - if (options.autofocus && !mobile) { display.input.focus() } - - // Override magic textarea content restore that IE sometimes does - // on our hidden textarea on reload - if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20) } - - registerEventHandlers(this) - ensureGlobalHandlers() - - startOperation(this) - this.curOp.forceUpdate = true - attachDoc(this, doc) - - if ((options.autofocus && !mobile) || this.hasFocus()) - { setTimeout(bind(onFocus, this), 20) } - else - { onBlur(this) } - - for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) - { optionHandlers[opt](this$1, options[opt], Init) } } - maybeUpdateLineNumberWidth(this) - if (options.finishInit) { options.finishInit(this) } - for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this$1) } - endOperation(this) - // Suppress optimizelegibility in Webkit, since it breaks text - // measuring on line wrapping boundaries. - if (webkit && options.lineWrapping && - getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") - { display.lineDiv.style.textRendering = "auto" } -} - -// The default configuration options. -CodeMirror.defaults = defaults -// Functions to run when options are changed. -CodeMirror.optionHandlers = optionHandlers - -// Attach the necessary event handlers when initializing the editor -function registerEventHandlers(cm) { - var d = cm.display - on(d.scroller, "mousedown", operation(cm, onMouseDown)) - // Older IE's will not fire a second mousedown for a double click - if (ie && ie_version < 11) - { on(d.scroller, "dblclick", operation(cm, function (e) { - if (signalDOMEvent(cm, e)) { return } - var pos = posFromMouse(cm, e) - if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } - e_preventDefault(e) - var word = cm.findWordAt(pos) - extendSelection(cm.doc, word.anchor, word.head) - })) } - else - { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }) } - // Some browsers fire contextmenu *after* opening the menu, at - // which point we can't mess with it anymore. Context menu is - // handled in onMouseDown for these browsers. - if (!captureRightClick) { on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }) } - - // Used to suppress mouse event handling when a touch happens - var touchFinished, prevTouch = {end: 0} - function finishTouch() { - if (d.activeTouch) { - touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000) - prevTouch = d.activeTouch - prevTouch.end = +new Date - } - } - function isMouseLikeTouchEvent(e) { - if (e.touches.length != 1) { return false } - var touch = e.touches[0] - return touch.radiusX <= 1 && touch.radiusY <= 1 - } - function farAway(touch, other) { - if (other.left == null) { return true } - var dx = other.left - touch.left, dy = other.top - touch.top - return dx * dx + dy * dy > 20 * 20 - } - on(d.scroller, "touchstart", function (e) { - if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) { - d.input.ensurePolled() - clearTimeout(touchFinished) - var now = +new Date - d.activeTouch = {start: now, moved: false, - prev: now - prevTouch.end <= 300 ? prevTouch : null} - if (e.touches.length == 1) { - d.activeTouch.left = e.touches[0].pageX - d.activeTouch.top = e.touches[0].pageY - } - } - }) - on(d.scroller, "touchmove", function () { - if (d.activeTouch) { d.activeTouch.moved = true } - }) - on(d.scroller, "touchend", function (e) { - var touch = d.activeTouch - if (touch && !eventInWidget(d, e) && touch.left != null && - !touch.moved && new Date - touch.start < 300) { - var pos = cm.coordsChar(d.activeTouch, "page"), range - if (!touch.prev || farAway(touch, touch.prev)) // Single tap - { range = new Range(pos, pos) } - else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap - { range = cm.findWordAt(pos) } - else // Triple tap - { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } - cm.setSelection(range.anchor, range.head) - cm.focus() - e_preventDefault(e) - } - finishTouch() - }) - on(d.scroller, "touchcancel", finishTouch) - - // Sync scrolling between fake scrollbars and real scrollable - // area, ensure viewport is updated when scrolling. - on(d.scroller, "scroll", function () { - if (d.scroller.clientHeight) { - updateScrollTop(cm, d.scroller.scrollTop) - setScrollLeft(cm, d.scroller.scrollLeft, true) - signal(cm, "scroll", cm) - } - }) - - // Listen to wheel events in order to try and update the viewport on time. - on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }) - on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }) - - // Prevent wrapper from ever scrolling - on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }) - - d.dragFunctions = { - enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e) }}, - over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e) }}, - start: function (e) { return onDragStart(cm, e); }, - drop: operation(cm, onDrop), - leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm) }} - } - - var inp = d.input.getField() - on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }) - on(inp, "keydown", operation(cm, onKeyDown)) - on(inp, "keypress", operation(cm, onKeyPress)) - on(inp, "focus", function (e) { return onFocus(cm, e); }) - on(inp, "blur", function (e) { return onBlur(cm, e); }) -} - -var initHooks = [] -CodeMirror.defineInitHook = function (f) { return initHooks.push(f); } - -// Indent the given line. The how parameter can be "smart", -// "add"/null, "subtract", or "prev". When aggressive is false -// (typically set to true for forced single-line indents), empty -// lines are not indented, and places where the mode returns Pass -// are left alone. -function indentLine(cm, n, how, aggressive) { - var doc = cm.doc, state - if (how == null) { how = "add" } - if (how == "smart") { - // Fall back to "prev" when the mode doesn't have an indentation - // method. - if (!doc.mode.indent) { how = "prev" } - else { state = getContextBefore(cm, n).state } - } - - var tabSize = cm.options.tabSize - var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize) - if (line.stateAfter) { line.stateAfter = null } - var curSpaceString = line.text.match(/^\s*/)[0], indentation - if (!aggressive && !/\S/.test(line.text)) { - indentation = 0 - how = "not" - } else if (how == "smart") { - indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text) - if (indentation == Pass || indentation > 150) { - if (!aggressive) { return } - how = "prev" - } - } - if (how == "prev") { - if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize) } - else { indentation = 0 } - } else if (how == "add") { - indentation = curSpace + cm.options.indentUnit - } else if (how == "subtract") { - indentation = curSpace - cm.options.indentUnit - } else if (typeof how == "number") { - indentation = curSpace + how - } - indentation = Math.max(0, indentation) - - var indentString = "", pos = 0 - if (cm.options.indentWithTabs) - { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t"} } - if (pos < indentation) { indentString += spaceStr(indentation - pos) } - - if (indentString != curSpaceString) { - replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input") - line.stateAfter = null - return true - } else { - // Ensure that, if the cursor was in the whitespace at the start - // of the line, it is moved to the end of that space. - for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { - var range = doc.sel.ranges[i$1] - if (range.head.line == n && range.head.ch < curSpaceString.length) { - var pos$1 = Pos(n, curSpaceString.length) - replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)) - break - } - } - } -} - -// This will be set to a {lineWise: bool, text: [string]} object, so -// that, when pasting, we know what kind of selections the copied -// text was made out of. -var lastCopied = null - -function setLastCopied(newLastCopied) { - lastCopied = newLastCopied -} - -function applyTextInput(cm, inserted, deleted, sel, origin) { - var doc = cm.doc - cm.display.shift = false - if (!sel) { sel = doc.sel } - - var paste = cm.state.pasteIncoming || origin == "paste" - var textLines = splitLinesAuto(inserted), multiPaste = null - // When pasing N lines into N selections, insert one line per selection - if (paste && sel.ranges.length > 1) { - if (lastCopied && lastCopied.text.join("\n") == inserted) { - if (sel.ranges.length % lastCopied.text.length == 0) { - multiPaste = [] - for (var i = 0; i < lastCopied.text.length; i++) - { multiPaste.push(doc.splitLines(lastCopied.text[i])) } - } - } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { - multiPaste = map(textLines, function (l) { return [l]; }) - } - } - - var updateInput - // Normal behavior is to insert the new text into every selection - for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { - var range = sel.ranges[i$1] - var from = range.from(), to = range.to() - if (range.empty()) { - if (deleted && deleted > 0) // Handle deletion - { from = Pos(from.line, from.ch - deleted) } - else if (cm.state.overwrite && !paste) // Handle overwrite - { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)) } - else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted) - { from = to = Pos(from.line, 0) } - } - updateInput = cm.curOp.updateInput - var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, - origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")} - makeChange(cm.doc, changeEvent) - signalLater(cm, "inputRead", cm, changeEvent) - } - if (inserted && !paste) - { triggerElectric(cm, inserted) } - - ensureCursorVisible(cm) - cm.curOp.updateInput = updateInput - cm.curOp.typing = true - cm.state.pasteIncoming = cm.state.cutIncoming = false -} - -function handlePaste(e, cm) { - var pasted = e.clipboardData && e.clipboardData.getData("Text") - if (pasted) { - e.preventDefault() - if (!cm.isReadOnly() && !cm.options.disableInput) - { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }) } - return true - } -} - -function triggerElectric(cm, inserted) { - // When an 'electric' character is inserted, immediately trigger a reindent - if (!cm.options.electricChars || !cm.options.smartIndent) { return } - var sel = cm.doc.sel - - for (var i = sel.ranges.length - 1; i >= 0; i--) { - var range = sel.ranges[i] - if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } - var mode = cm.getModeAt(range.head) - var indented = false - if (mode.electricChars) { - for (var j = 0; j < mode.electricChars.length; j++) - { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { - indented = indentLine(cm, range.head.line, "smart") - break - } } - } else if (mode.electricInput) { - if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) - { indented = indentLine(cm, range.head.line, "smart") } - } - if (indented) { signalLater(cm, "electricInput", cm, range.head.line) } - } -} - -function copyableRanges(cm) { - var text = [], ranges = [] - for (var i = 0; i < cm.doc.sel.ranges.length; i++) { - var line = cm.doc.sel.ranges[i].head.line - var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)} - ranges.push(lineRange) - text.push(cm.getRange(lineRange.anchor, lineRange.head)) - } - return {text: text, ranges: ranges} -} - -function disableBrowserMagic(field, spellcheck) { - field.setAttribute("autocorrect", "off") - field.setAttribute("autocapitalize", "off") - field.setAttribute("spellcheck", !!spellcheck) -} - -function hiddenTextarea() { - var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none") - var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;") - // The textarea is kept positioned near the cursor to prevent the - // fact that it'll be scrolled into view on input from scrolling - // our fake cursor out of view. On webkit, when wrap=off, paste is - // very slow. So make the area wide instead. - if (webkit) { te.style.width = "1000px" } - else { te.setAttribute("wrap", "off") } - // If border: 0; -- iOS fails to open keyboard (issue #1287) - if (ios) { te.style.border = "1px solid black" } - disableBrowserMagic(te) - return div -} - -// The publicly visible API. Note that methodOp(f) means -// 'wrap f in an operation, performed on its `this` parameter'. - -// This is not the complete set of editor methods. Most of the -// methods defined on the Doc type are also injected into -// CodeMirror.prototype, for backwards compatibility and -// convenience. - -function addEditorMethods(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers - - var helpers = CodeMirror.helpers = {} - - CodeMirror.prototype = { - constructor: CodeMirror, - focus: function(){window.focus(); this.display.input.focus()}, - - setOption: function(option, value) { - var options = this.options, old = options[option] - if (options[option] == value && option != "mode") { return } - options[option] = value - if (optionHandlers.hasOwnProperty(option)) - { operation(this, optionHandlers[option])(this, value, old) } - signal(this, "optionChange", this, option) - }, - - getOption: function(option) {return this.options[option]}, - getDoc: function() {return this.doc}, - - addKeyMap: function(map, bottom) { - this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)) - }, - removeKeyMap: function(map) { - var maps = this.state.keyMaps - for (var i = 0; i < maps.length; ++i) - { if (maps[i] == map || maps[i].name == map) { - maps.splice(i, 1) - return true - } } - }, - - addOverlay: methodOp(function(spec, options) { - var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec) - if (mode.startState) { throw new Error("Overlays may not be stateful.") } - insertSorted(this.state.overlays, - {mode: mode, modeSpec: spec, opaque: options && options.opaque, - priority: (options && options.priority) || 0}, - function (overlay) { return overlay.priority; }) - this.state.modeGen++ - regChange(this) - }), - removeOverlay: methodOp(function(spec) { - var this$1 = this; - - var overlays = this.state.overlays - for (var i = 0; i < overlays.length; ++i) { - var cur = overlays[i].modeSpec - if (cur == spec || typeof spec == "string" && cur.name == spec) { - overlays.splice(i, 1) - this$1.state.modeGen++ - regChange(this$1) - return - } - } - }), - - indentLine: methodOp(function(n, dir, aggressive) { - if (typeof dir != "string" && typeof dir != "number") { - if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev" } - else { dir = dir ? "add" : "subtract" } - } - if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive) } - }), - indentSelection: methodOp(function(how) { - var this$1 = this; - - var ranges = this.doc.sel.ranges, end = -1 - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i] - if (!range.empty()) { - var from = range.from(), to = range.to() - var start = Math.max(end, from.line) - end = Math.min(this$1.lastLine(), to.line - (to.ch ? 0 : 1)) + 1 - for (var j = start; j < end; ++j) - { indentLine(this$1, j, how) } - var newRanges = this$1.doc.sel.ranges - if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) - { replaceOneSelection(this$1.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll) } - } else if (range.head.line > end) { - indentLine(this$1, range.head.line, how, true) - end = range.head.line - if (i == this$1.doc.sel.primIndex) { ensureCursorVisible(this$1) } - } - } - }), - - // Fetch the parser token for a given character. Useful for hacks - // that want to inspect the mode state (say, for completion). - getTokenAt: function(pos, precise) { - return takeToken(this, pos, precise) - }, - - getLineTokens: function(line, precise) { - return takeToken(this, Pos(line), precise, true) - }, - - getTokenTypeAt: function(pos) { - pos = clipPos(this.doc, pos) - var styles = getLineStyles(this, getLine(this.doc, pos.line)) - var before = 0, after = (styles.length - 1) / 2, ch = pos.ch - var type - if (ch == 0) { type = styles[2] } - else { for (;;) { - var mid = (before + after) >> 1 - if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid } - else if (styles[mid * 2 + 1] < ch) { before = mid + 1 } - else { type = styles[mid * 2 + 2]; break } - } } - var cut = type ? type.indexOf("overlay ") : -1 - return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) - }, - - getModeAt: function(pos) { - var mode = this.doc.mode - if (!mode.innerMode) { return mode } - return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode - }, - - getHelper: function(pos, type) { - return this.getHelpers(pos, type)[0] - }, - - getHelpers: function(pos, type) { - var this$1 = this; - - var found = [] - if (!helpers.hasOwnProperty(type)) { return found } - var help = helpers[type], mode = this.getModeAt(pos) - if (typeof mode[type] == "string") { - if (help[mode[type]]) { found.push(help[mode[type]]) } - } else if (mode[type]) { - for (var i = 0; i < mode[type].length; i++) { - var val = help[mode[type][i]] - if (val) { found.push(val) } - } - } else if (mode.helperType && help[mode.helperType]) { - found.push(help[mode.helperType]) - } else if (help[mode.name]) { - found.push(help[mode.name]) - } - for (var i$1 = 0; i$1 < help._global.length; i$1++) { - var cur = help._global[i$1] - if (cur.pred(mode, this$1) && indexOf(found, cur.val) == -1) - { found.push(cur.val) } - } - return found - }, - - getStateAfter: function(line, precise) { - var doc = this.doc - line = clipLine(doc, line == null ? doc.first + doc.size - 1: line) - return getContextBefore(this, line + 1, precise).state - }, - - cursorCoords: function(start, mode) { - var pos, range = this.doc.sel.primary() - if (start == null) { pos = range.head } - else if (typeof start == "object") { pos = clipPos(this.doc, start) } - else { pos = start ? range.from() : range.to() } - return cursorCoords(this, pos, mode || "page") - }, - - charCoords: function(pos, mode) { - return charCoords(this, clipPos(this.doc, pos), mode || "page") - }, - - coordsChar: function(coords, mode) { - coords = fromCoordSystem(this, coords, mode || "page") - return coordsChar(this, coords.left, coords.top) - }, - - lineAtHeight: function(height, mode) { - height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top - return lineAtHeight(this.doc, height + this.display.viewOffset) - }, - heightAtLine: function(line, mode, includeWidgets) { - var end = false, lineObj - if (typeof line == "number") { - var last = this.doc.first + this.doc.size - 1 - if (line < this.doc.first) { line = this.doc.first } - else if (line > last) { line = last; end = true } - lineObj = getLine(this.doc, line) - } else { - lineObj = line - } - return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + - (end ? this.doc.height - heightAtLine(lineObj) : 0) - }, - - defaultTextHeight: function() { return textHeight(this.display) }, - defaultCharWidth: function() { return charWidth(this.display) }, - - getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, - - addWidget: function(pos, node, scroll, vert, horiz) { - var display = this.display - pos = cursorCoords(this, clipPos(this.doc, pos)) - var top = pos.bottom, left = pos.left - node.style.position = "absolute" - node.setAttribute("cm-ignore-events", "true") - this.display.input.setUneditable(node) - display.sizer.appendChild(node) - if (vert == "over") { - top = pos.top - } else if (vert == "above" || vert == "near") { - var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), - hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth) - // Default to positioning above (if specified and possible); otherwise default to positioning below - if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) - { top = pos.top - node.offsetHeight } - else if (pos.bottom + node.offsetHeight <= vspace) - { top = pos.bottom } - if (left + node.offsetWidth > hspace) - { left = hspace - node.offsetWidth } - } - node.style.top = top + "px" - node.style.left = node.style.right = "" - if (horiz == "right") { - left = display.sizer.clientWidth - node.offsetWidth - node.style.right = "0px" - } else { - if (horiz == "left") { left = 0 } - else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2 } - node.style.left = left + "px" - } - if (scroll) - { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}) } - }, - - triggerOnKeyDown: methodOp(onKeyDown), - triggerOnKeyPress: methodOp(onKeyPress), - triggerOnKeyUp: onKeyUp, - triggerOnMouseDown: methodOp(onMouseDown), - - execCommand: function(cmd) { - if (commands.hasOwnProperty(cmd)) - { return commands[cmd].call(null, this) } - }, - - triggerElectric: methodOp(function(text) { triggerElectric(this, text) }), - - findPosH: function(from, amount, unit, visually) { - var this$1 = this; - - var dir = 1 - if (amount < 0) { dir = -1; amount = -amount } - var cur = clipPos(this.doc, from) - for (var i = 0; i < amount; ++i) { - cur = findPosH(this$1.doc, cur, dir, unit, visually) - if (cur.hitSide) { break } - } - return cur - }, - - moveH: methodOp(function(dir, unit) { - var this$1 = this; - - this.extendSelectionsBy(function (range) { - if (this$1.display.shift || this$1.doc.extend || range.empty()) - { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } - else - { return dir < 0 ? range.from() : range.to() } - }, sel_move) - }), - - deleteH: methodOp(function(dir, unit) { - var sel = this.doc.sel, doc = this.doc - if (sel.somethingSelected()) - { doc.replaceSelection("", null, "+delete") } - else - { deleteNearSelection(this, function (range) { - var other = findPosH(doc, range.head, dir, unit, false) - return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} - }) } - }), - - findPosV: function(from, amount, unit, goalColumn) { - var this$1 = this; - - var dir = 1, x = goalColumn - if (amount < 0) { dir = -1; amount = -amount } - var cur = clipPos(this.doc, from) - for (var i = 0; i < amount; ++i) { - var coords = cursorCoords(this$1, cur, "div") - if (x == null) { x = coords.left } - else { coords.left = x } - cur = findPosV(this$1, coords, dir, unit) - if (cur.hitSide) { break } - } - return cur - }, - - moveV: methodOp(function(dir, unit) { - var this$1 = this; - - var doc = this.doc, goals = [] - var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected() - doc.extendSelectionsBy(function (range) { - if (collapse) - { return dir < 0 ? range.from() : range.to() } - var headPos = cursorCoords(this$1, range.head, "div") - if (range.goalColumn != null) { headPos.left = range.goalColumn } - goals.push(headPos.left) - var pos = findPosV(this$1, headPos, dir, unit) - if (unit == "page" && range == doc.sel.primary()) - { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top) } - return pos - }, sel_move) - if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) - { doc.sel.ranges[i].goalColumn = goals[i] } } - }), - - // Find the word at the given position (as returned by coordsChar). - findWordAt: function(pos) { - var doc = this.doc, line = getLine(doc, pos.line).text - var start = pos.ch, end = pos.ch - if (line) { - var helper = this.getHelper(pos, "wordChars") - if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end } - var startChar = line.charAt(start) - var check = isWordChar(startChar, helper) - ? function (ch) { return isWordChar(ch, helper); } - : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } - : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); } - while (start > 0 && check(line.charAt(start - 1))) { --start } - while (end < line.length && check(line.charAt(end))) { ++end } - } - return new Range(Pos(pos.line, start), Pos(pos.line, end)) - }, - - toggleOverwrite: function(value) { - if (value != null && value == this.state.overwrite) { return } - if (this.state.overwrite = !this.state.overwrite) - { addClass(this.display.cursorDiv, "CodeMirror-overwrite") } - else - { rmClass(this.display.cursorDiv, "CodeMirror-overwrite") } - - signal(this, "overwriteToggle", this, this.state.overwrite) - }, - hasFocus: function() { return this.display.input.getField() == activeElt() }, - isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, - - scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y) }), - getScrollInfo: function() { - var scroller = this.display.scroller - return {left: scroller.scrollLeft, top: scroller.scrollTop, - height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, - width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, - clientHeight: displayHeight(this), clientWidth: displayWidth(this)} - }, - - scrollIntoView: methodOp(function(range, margin) { - if (range == null) { - range = {from: this.doc.sel.primary().head, to: null} - if (margin == null) { margin = this.options.cursorScrollMargin } - } else if (typeof range == "number") { - range = {from: Pos(range, 0), to: null} - } else if (range.from == null) { - range = {from: range, to: null} - } - if (!range.to) { range.to = range.from } - range.margin = margin || 0 - - if (range.from.line != null) { - scrollToRange(this, range) - } else { - scrollToCoordsRange(this, range.from, range.to, range.margin) - } - }), - - setSize: methodOp(function(width, height) { - var this$1 = this; - - var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; } - if (width != null) { this.display.wrapper.style.width = interpret(width) } - if (height != null) { this.display.wrapper.style.height = interpret(height) } - if (this.options.lineWrapping) { clearLineMeasurementCache(this) } - var lineNo = this.display.viewFrom - this.doc.iter(lineNo, this.display.viewTo, function (line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) - { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } - ++lineNo - }) - this.curOp.forceUpdate = true - signal(this, "refresh", this) - }), - - operation: function(f){return runInOp(this, f)}, - startOperation: function(){return startOperation(this)}, - endOperation: function(){return endOperation(this)}, - - refresh: methodOp(function() { - var oldHeight = this.display.cachedTextHeight - regChange(this) - this.curOp.forceUpdate = true - clearCaches(this) - scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) - updateGutterSpace(this) - if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) - { estimateLineHeights(this) } - signal(this, "refresh", this) - }), - - swapDoc: methodOp(function(doc) { - var old = this.doc - old.cm = null - attachDoc(this, doc) - clearCaches(this) - this.display.input.reset() - scrollToCoords(this, doc.scrollLeft, doc.scrollTop) - this.curOp.forceScroll = true - signalLater(this, "swapDoc", this, old) - return old - }), - - getInputField: function(){return this.display.input.getField()}, - getWrapperElement: function(){return this.display.wrapper}, - getScrollerElement: function(){return this.display.scroller}, - getGutterElement: function(){return this.display.gutters} - } - eventMixin(CodeMirror) - - CodeMirror.registerHelper = function(type, name, value) { - if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []} } - helpers[type][name] = value - } - CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { - CodeMirror.registerHelper(type, name, value) - helpers[type]._global.push({pred: predicate, val: value}) - } -} - -// Used for horizontal relative motion. Dir is -1 or 1 (left or -// right), unit can be "char", "column" (like char, but doesn't -// cross line boundaries), "word" (across next word), or "group" (to -// the start of next group of word or non-word-non-whitespace -// chars). The visually param controls whether, in right-to-left -// text, direction 1 means to move towards the next index in the -// string, or towards the character to the right of the current -// position. The resulting position will have a hitSide=true -// property if it reached the end of the document. -function findPosH(doc, pos, dir, unit, visually) { - var oldPos = pos - var origDir = dir - var lineObj = getLine(doc, pos.line) - function findNextLine() { - var l = pos.line + dir - if (l < doc.first || l >= doc.first + doc.size) { return false } - pos = new Pos(l, pos.ch, pos.sticky) - return lineObj = getLine(doc, l) - } - function moveOnce(boundToLine) { - var next - if (visually) { - next = moveVisually(doc.cm, lineObj, pos, dir) - } else { - next = moveLogically(lineObj, pos, dir) - } - if (next == null) { - if (!boundToLine && findNextLine()) - { pos = endOfLine(visually, doc.cm, lineObj, pos.line, dir) } - else - { return false } - } else { - pos = next - } - return true - } - - if (unit == "char") { - moveOnce() - } else if (unit == "column") { - moveOnce(true) - } else if (unit == "word" || unit == "group") { - var sawType = null, group = unit == "group" - var helper = doc.cm && doc.cm.getHelper(pos, "wordChars") - for (var first = true;; first = false) { - if (dir < 0 && !moveOnce(!first)) { break } - var cur = lineObj.text.charAt(pos.ch) || "\n" - var type = isWordChar(cur, helper) ? "w" - : group && cur == "\n" ? "n" - : !group || /\s/.test(cur) ? null - : "p" - if (group && !first && !type) { type = "s" } - if (sawType && sawType != type) { - if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after"} - break - } - - if (type) { sawType = type } - if (dir > 0 && !moveOnce(!first)) { break } - } - } - var result = skipAtomic(doc, pos, oldPos, origDir, true) - if (equalCursorPos(oldPos, result)) { result.hitSide = true } - return result -} - -// For relative vertical movement. Dir may be -1 or 1. Unit can be -// "page" or "line". The resulting position will have a hitSide=true -// property if it reached the end of the document. -function findPosV(cm, pos, dir, unit) { - var doc = cm.doc, x = pos.left, y - if (unit == "page") { - var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight) - var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3) - y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount - - } else if (unit == "line") { - y = dir > 0 ? pos.bottom + 3 : pos.top - 3 - } - var target - for (;;) { - target = coordsChar(cm, x, y) - if (!target.outside) { break } - if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } - y += dir * 5 - } - return target -} - -// CONTENTEDITABLE INPUT STYLE - -var ContentEditableInput = function(cm) { - this.cm = cm - this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null - this.polling = new Delayed() - this.composing = null - this.gracePeriod = false - this.readDOMTimeout = null -}; - -ContentEditableInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = input.cm - var div = input.div = display.lineDiv - disableBrowserMagic(div, cm.options.spellcheck) - - on(div, "paste", function (e) { - if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - // IE doesn't fire input events, so we schedule a read for the pasted content in this way - if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20) } - }) - - on(div, "compositionstart", function (e) { - this$1.composing = {data: e.data, done: false} - }) - on(div, "compositionupdate", function (e) { - if (!this$1.composing) { this$1.composing = {data: e.data, done: false} } - }) - on(div, "compositionend", function (e) { - if (this$1.composing) { - if (e.data != this$1.composing.data) { this$1.readFromDOMSoon() } - this$1.composing.done = true - } - }) - - on(div, "touchstart", function () { return input.forceCompositionEnd(); }) - - on(div, "input", function () { - if (!this$1.composing) { this$1.readFromDOMSoon() } - }) - - function onCopyCut(e) { - if (signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}) - if (e.type == "cut") { cm.replaceSelection("", null, "cut") } - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm) - setLastCopied({lineWise: true, text: ranges.text}) - if (e.type == "cut") { - cm.operation(function () { - cm.setSelections(ranges.ranges, 0, sel_dontScroll) - cm.replaceSelection("", null, "cut") - }) - } - } - if (e.clipboardData) { - e.clipboardData.clearData() - var content = lastCopied.text.join("\n") - // iOS exposes the clipboard API, but seems to discard content inserted into it - e.clipboardData.setData("Text", content) - if (e.clipboardData.getData("Text") == content) { - e.preventDefault() - return - } - } - // Old-fashioned briefly-focus-a-textarea hack - var kludge = hiddenTextarea(), te = kludge.firstChild - cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild) - te.value = lastCopied.text.join("\n") - var hadFocus = document.activeElement - selectInput(te) - setTimeout(function () { - cm.display.lineSpace.removeChild(kludge) - hadFocus.focus() - if (hadFocus == div) { input.showPrimarySelection() } - }, 50) - } - on(div, "copy", onCopyCut) - on(div, "cut", onCopyCut) -}; - -ContentEditableInput.prototype.prepareSelection = function () { - var result = prepareSelection(this.cm, false) - result.focus = this.cm.state.focused - return result -}; - -ContentEditableInput.prototype.showSelection = function (info, takeFocus) { - if (!info || !this.cm.display.view.length) { return } - if (info.focus || takeFocus) { this.showPrimarySelection() } - this.showMultipleSelections(info) -}; - -ContentEditableInput.prototype.showPrimarySelection = function () { - var sel = window.getSelection(), cm = this.cm, prim = cm.doc.sel.primary() - var from = prim.from(), to = prim.to() - - if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { - sel.removeAllRanges() - return - } - - var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) - var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset) - if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && - cmp(minPos(curAnchor, curFocus), from) == 0 && - cmp(maxPos(curAnchor, curFocus), to) == 0) - { return } - - var view = cm.display.view - var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || - {node: view[0].measure.map[2], offset: 0} - var end = to.line < cm.display.viewTo && posToDOM(cm, to) - if (!end) { - var measure = view[view.length - 1].measure - var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map - end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]} - } - - if (!start || !end) { - sel.removeAllRanges() - return - } - - var old = sel.rangeCount && sel.getRangeAt(0), rng - try { rng = range(start.node, start.offset, end.offset, end.node) } - catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible - if (rng) { - if (!gecko && cm.state.focused) { - sel.collapse(start.node, start.offset) - if (!rng.collapsed) { - sel.removeAllRanges() - sel.addRange(rng) - } - } else { - sel.removeAllRanges() - sel.addRange(rng) - } - if (old && sel.anchorNode == null) { sel.addRange(old) } - else if (gecko) { this.startGracePeriod() } - } - this.rememberSelection() -}; - -ContentEditableInput.prototype.startGracePeriod = function () { - var this$1 = this; - - clearTimeout(this.gracePeriod) - this.gracePeriod = setTimeout(function () { - this$1.gracePeriod = false - if (this$1.selectionChanged()) - { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }) } - }, 20) -}; - -ContentEditableInput.prototype.showMultipleSelections = function (info) { - removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors) - removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection) -}; - -ContentEditableInput.prototype.rememberSelection = function () { - var sel = window.getSelection() - this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset - this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset -}; - -ContentEditableInput.prototype.selectionInEditor = function () { - var sel = window.getSelection() - if (!sel.rangeCount) { return false } - var node = sel.getRangeAt(0).commonAncestorContainer - return contains(this.div, node) -}; - -ContentEditableInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor") { - if (!this.selectionInEditor()) - { this.showSelection(this.prepareSelection(), true) } - this.div.focus() - } -}; -ContentEditableInput.prototype.blur = function () { this.div.blur() }; -ContentEditableInput.prototype.getField = function () { return this.div }; - -ContentEditableInput.prototype.supportsTouch = function () { return true }; - -ContentEditableInput.prototype.receivedFocus = function () { - var input = this - if (this.selectionInEditor()) - { this.pollSelection() } - else - { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }) } - - function poll() { - if (input.cm.state.focused) { - input.pollSelection() - input.polling.set(input.cm.options.pollInterval, poll) - } - } - this.polling.set(this.cm.options.pollInterval, poll) -}; - -ContentEditableInput.prototype.selectionChanged = function () { - var sel = window.getSelection() - return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || - sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset -}; - -ContentEditableInput.prototype.pollSelection = function () { - if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } - var sel = window.getSelection(), cm = this.cm - // On Android Chrome (version 56, at least), backspacing into an - // uneditable block element will put the cursor in that element, - // and then, because it's not editable, hide the virtual keyboard. - // Because Android doesn't allow us to actually detect backspace - // presses in a sane way, this code checks for when that happens - // and simulates a backspace press in this case. - if (android && chrome && this.cm.options.gutters.length && isInGutter(sel.anchorNode)) { - this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}) - this.blur() - this.focus() - return - } - if (this.composing) { return } - this.rememberSelection() - var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) - var head = domToPos(cm, sel.focusNode, sel.focusOffset) - if (anchor && head) { runInOp(cm, function () { - setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll) - if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true } - }) } -}; - -ContentEditableInput.prototype.pollContent = function () { - if (this.readDOMTimeout != null) { - clearTimeout(this.readDOMTimeout) - this.readDOMTimeout = null - } - - var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary() - var from = sel.from(), to = sel.to() - if (from.ch == 0 && from.line > cm.firstLine()) - { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length) } - if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) - { to = Pos(to.line + 1, 0) } - if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } - - var fromIndex, fromLine, fromNode - if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { - fromLine = lineNo(display.view[0].line) - fromNode = display.view[0].node - } else { - fromLine = lineNo(display.view[fromIndex].line) - fromNode = display.view[fromIndex - 1].node.nextSibling - } - var toIndex = findViewIndex(cm, to.line) - var toLine, toNode - if (toIndex == display.view.length - 1) { - toLine = display.viewTo - 1 - toNode = display.lineDiv.lastChild - } else { - toLine = lineNo(display.view[toIndex + 1].line) - 1 - toNode = display.view[toIndex + 1].node.previousSibling - } - - if (!fromNode) { return false } - var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)) - var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)) - while (newText.length > 1 && oldText.length > 1) { - if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- } - else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ } - else { break } - } - - var cutFront = 0, cutEnd = 0 - var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length) - while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) - { ++cutFront } - var newBot = lst(newText), oldBot = lst(oldText) - var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), - oldBot.length - (oldText.length == 1 ? cutFront : 0)) - while (cutEnd < maxCutEnd && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) - { ++cutEnd } - // Try to move start of change to start of selection if ambiguous - if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { - while (cutFront && cutFront > from.ch && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { - cutFront-- - cutEnd++ - } - } - - newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "") - newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "") - - var chFrom = Pos(fromLine, cutFront) - var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0) - if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { - replaceRange(cm.doc, newText, chFrom, chTo, "+input") - return true - } -}; - -ContentEditableInput.prototype.ensurePolled = function () { - this.forceCompositionEnd() -}; -ContentEditableInput.prototype.reset = function () { - this.forceCompositionEnd() -}; -ContentEditableInput.prototype.forceCompositionEnd = function () { - if (!this.composing) { return } - clearTimeout(this.readDOMTimeout) - this.composing = null - this.updateFromDOM() - this.div.blur() - this.div.focus() -}; -ContentEditableInput.prototype.readFromDOMSoon = function () { - var this$1 = this; - - if (this.readDOMTimeout != null) { return } - this.readDOMTimeout = setTimeout(function () { - this$1.readDOMTimeout = null - if (this$1.composing) { - if (this$1.composing.done) { this$1.composing = null } - else { return } - } - this$1.updateFromDOM() - }, 80) -}; - -ContentEditableInput.prototype.updateFromDOM = function () { - var this$1 = this; - - if (this.cm.isReadOnly() || !this.pollContent()) - { runInOp(this.cm, function () { return regChange(this$1.cm); }) } -}; - -ContentEditableInput.prototype.setUneditable = function (node) { - node.contentEditable = "false" -}; - -ContentEditableInput.prototype.onKeyPress = function (e) { - if (e.charCode == 0) { return } - e.preventDefault() - if (!this.cm.isReadOnly()) - { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) } -}; - -ContentEditableInput.prototype.readOnlyChanged = function (val) { - this.div.contentEditable = String(val != "nocursor") -}; - -ContentEditableInput.prototype.onContextMenu = function () {}; -ContentEditableInput.prototype.resetPosition = function () {}; - -ContentEditableInput.prototype.needsContentAttribute = true - -function posToDOM(cm, pos) { - var view = findViewForLine(cm, pos.line) - if (!view || view.hidden) { return null } - var line = getLine(cm.doc, pos.line) - var info = mapFromLineView(view, line, pos.line) - - var order = getOrder(line, cm.doc.direction), side = "left" - if (order) { - var partPos = getBidiPartAt(order, pos.ch) - side = partPos % 2 ? "right" : "left" - } - var result = nodeAndOffsetInLineMap(info.map, pos.ch, side) - result.offset = result.collapse == "right" ? result.end : result.start - return result -} - -function isInGutter(node) { - for (var scan = node; scan; scan = scan.parentNode) - { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } - return false -} - -function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } - -function domTextBetween(cm, from, to, fromLine, toLine) { - var text = "", closing = false, lineSep = cm.doc.lineSeparator() - function recognizeMarker(id) { return function (marker) { return marker.id == id; } } - function close() { - if (closing) { - text += lineSep - closing = false - } - } - function addText(str) { - if (str) { - close() - text += str - } - } - function walk(node) { - if (node.nodeType == 1) { - var cmText = node.getAttribute("cm-text") - if (cmText != null) { - addText(cmText || node.textContent.replace(/\u200b/g, "")) - return - } - var markerID = node.getAttribute("cm-marker"), range - if (markerID) { - var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)) - if (found.length && (range = found[0].find())) - { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)) } - return - } - if (node.getAttribute("contenteditable") == "false") { return } - var isBlock = /^(pre|div|p)$/i.test(node.nodeName) - if (isBlock) { close() } - for (var i = 0; i < node.childNodes.length; i++) - { walk(node.childNodes[i]) } - if (isBlock) { closing = true } - } else if (node.nodeType == 3) { - addText(node.nodeValue) - } - } - for (;;) { - walk(from) - if (from == to) { break } - from = from.nextSibling - } - return text -} - -function domToPos(cm, node, offset) { - var lineNode - if (node == cm.display.lineDiv) { - lineNode = cm.display.lineDiv.childNodes[offset] - if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } - node = null; offset = 0 - } else { - for (lineNode = node;; lineNode = lineNode.parentNode) { - if (!lineNode || lineNode == cm.display.lineDiv) { return null } - if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } - } - } - for (var i = 0; i < cm.display.view.length; i++) { - var lineView = cm.display.view[i] - if (lineView.node == lineNode) - { return locateNodeInLineView(lineView, node, offset) } - } -} - -function locateNodeInLineView(lineView, node, offset) { - var wrapper = lineView.text.firstChild, bad = false - if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } - if (node == wrapper) { - bad = true - node = wrapper.childNodes[offset] - offset = 0 - if (!node) { - var line = lineView.rest ? lst(lineView.rest) : lineView.line - return badPos(Pos(lineNo(line), line.text.length), bad) - } - } - - var textNode = node.nodeType == 3 ? node : null, topNode = node - if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { - textNode = node.firstChild - if (offset) { offset = textNode.nodeValue.length } - } - while (topNode.parentNode != wrapper) { topNode = topNode.parentNode } - var measure = lineView.measure, maps = measure.maps - - function find(textNode, topNode, offset) { - for (var i = -1; i < (maps ? maps.length : 0); i++) { - var map = i < 0 ? measure.map : maps[i] - for (var j = 0; j < map.length; j += 3) { - var curNode = map[j + 2] - if (curNode == textNode || curNode == topNode) { - var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]) - var ch = map[j] + offset - if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)] } - return Pos(line, ch) - } - } - } - } - var found = find(textNode, topNode, offset) - if (found) { return badPos(found, bad) } - - // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems - for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { - found = find(after, after.firstChild, 0) - if (found) - { return badPos(Pos(found.line, found.ch - dist), bad) } - else - { dist += after.textContent.length } - } - for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { - found = find(before, before.firstChild, -1) - if (found) - { return badPos(Pos(found.line, found.ch + dist$1), bad) } - else - { dist$1 += before.textContent.length } - } -} - -// TEXTAREA INPUT STYLE - -var TextareaInput = function(cm) { - this.cm = cm - // See input.poll and input.reset - this.prevInput = "" - - // Flag that indicates whether we expect input to appear real soon - // now (after some event like 'keypress' or 'input') and are - // polling intensively. - this.pollingFast = false - // Self-resetting timeout for the poller - this.polling = new Delayed() - // Used to work around IE issue with selection being forgotten when focus moves away from textarea - this.hasSelection = false - this.composing = null -}; - -TextareaInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = this.cm - - // Wraps and hides input textarea - var div = this.wrapper = hiddenTextarea() - // The semihidden textarea that is focused when the editor is - // focused, and receives input. - var te = this.textarea = div.firstChild - display.wrapper.insertBefore(div, display.wrapper.firstChild) - - // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) - if (ios) { te.style.width = "0px" } - - on(te, "input", function () { - if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null } - input.poll() - }) - - on(te, "paste", function (e) { - if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - - cm.state.pasteIncoming = true - input.fastPoll() - }) - - function prepareCopyCut(e) { - if (signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}) - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm) - setLastCopied({lineWise: true, text: ranges.text}) - if (e.type == "cut") { - cm.setSelections(ranges.ranges, null, sel_dontScroll) - } else { - input.prevInput = "" - te.value = ranges.text.join("\n") - selectInput(te) - } - } - if (e.type == "cut") { cm.state.cutIncoming = true } - } - on(te, "cut", prepareCopyCut) - on(te, "copy", prepareCopyCut) - - on(display.scroller, "paste", function (e) { - if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } - cm.state.pasteIncoming = true - input.focus() - }) - - // Prevent normal selection in the editor (we handle our own) - on(display.lineSpace, "selectstart", function (e) { - if (!eventInWidget(display, e)) { e_preventDefault(e) } - }) - - on(te, "compositionstart", function () { - var start = cm.getCursor("from") - if (input.composing) { input.composing.range.clear() } - input.composing = { - start: start, - range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) - } - }) - on(te, "compositionend", function () { - if (input.composing) { - input.poll() - input.composing.range.clear() - input.composing = null - } - }) -}; - -TextareaInput.prototype.prepareSelection = function () { - // Redraw the selection and/or cursor - var cm = this.cm, display = cm.display, doc = cm.doc - var result = prepareSelection(cm) - - // Move the hidden textarea near the cursor to prevent scrolling artifacts - if (cm.options.moveInputWithCursor) { - var headPos = cursorCoords(cm, doc.sel.primary().head, "div") - var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect() - result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, - headPos.top + lineOff.top - wrapOff.top)) - result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, - headPos.left + lineOff.left - wrapOff.left)) - } - - return result -}; - -TextareaInput.prototype.showSelection = function (drawn) { - var cm = this.cm, display = cm.display - removeChildrenAndAdd(display.cursorDiv, drawn.cursors) - removeChildrenAndAdd(display.selectionDiv, drawn.selection) - if (drawn.teTop != null) { - this.wrapper.style.top = drawn.teTop + "px" - this.wrapper.style.left = drawn.teLeft + "px" - } -}; - -// Reset the input to correspond to the selection (or to be empty, -// when not typing and nothing is selected) -TextareaInput.prototype.reset = function (typing) { - if (this.contextMenuPending || this.composing) { return } - var cm = this.cm - if (cm.somethingSelected()) { - this.prevInput = "" - var content = cm.getSelection() - this.textarea.value = content - if (cm.state.focused) { selectInput(this.textarea) } - if (ie && ie_version >= 9) { this.hasSelection = content } - } else if (!typing) { - this.prevInput = this.textarea.value = "" - if (ie && ie_version >= 9) { this.hasSelection = null } - } -}; - -TextareaInput.prototype.getField = function () { return this.textarea }; - -TextareaInput.prototype.supportsTouch = function () { return false }; - -TextareaInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { - try { this.textarea.focus() } - catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM - } -}; - -TextareaInput.prototype.blur = function () { this.textarea.blur() }; - -TextareaInput.prototype.resetPosition = function () { - this.wrapper.style.top = this.wrapper.style.left = 0 -}; - -TextareaInput.prototype.receivedFocus = function () { this.slowPoll() }; - -// Poll for input changes, using the normal rate of polling. This -// runs as long as the editor is focused. -TextareaInput.prototype.slowPoll = function () { - var this$1 = this; - - if (this.pollingFast) { return } - this.polling.set(this.cm.options.pollInterval, function () { - this$1.poll() - if (this$1.cm.state.focused) { this$1.slowPoll() } - }) -}; - -// When an event has just come in that is likely to add or change -// something in the input textarea, we poll faster, to ensure that -// the change appears on the screen quickly. -TextareaInput.prototype.fastPoll = function () { - var missed = false, input = this - input.pollingFast = true - function p() { - var changed = input.poll() - if (!changed && !missed) {missed = true; input.polling.set(60, p)} - else {input.pollingFast = false; input.slowPoll()} - } - input.polling.set(20, p) -}; - -// Read input from the textarea, and update the document to match. -// When something is selected, it is present in the textarea, and -// selected (unless it is huge, in which case a placeholder is -// used). When nothing is selected, the cursor sits after previously -// seen text (can be empty), which is stored in prevInput (we must -// not reset the textarea when typing, because that breaks IME). -TextareaInput.prototype.poll = function () { - var this$1 = this; - - var cm = this.cm, input = this.textarea, prevInput = this.prevInput - // Since this is called a *lot*, try to bail out as cheaply as - // possible when it is clear that nothing happened. hasSelection - // will be the case when there is a lot of text in the textarea, - // in which case reading its value would be expensive. - if (this.contextMenuPending || !cm.state.focused || - (hasSelection(input) && !prevInput && !this.composing) || - cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) - { return false } - - var text = input.value - // If nothing changed, bail. - if (text == prevInput && !cm.somethingSelected()) { return false } - // Work around nonsensical selection resetting in IE9/10, and - // inexplicable appearance of private area unicode characters on - // some key combos in Mac (#2689). - if (ie && ie_version >= 9 && this.hasSelection === text || - mac && /[\uf700-\uf7ff]/.test(text)) { - cm.display.input.reset() - return false - } - - if (cm.doc.sel == cm.display.selForContextMenu) { - var first = text.charCodeAt(0) - if (first == 0x200b && !prevInput) { prevInput = "\u200b" } - if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } - } - // Find the part of the input that is actually new - var same = 0, l = Math.min(prevInput.length, text.length) - while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same } - - runInOp(cm, function () { - applyTextInput(cm, text.slice(same), prevInput.length - same, - null, this$1.composing ? "*compose" : null) - - // Don't leave long text in the textarea, since it makes further polling slow - if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = "" } - else { this$1.prevInput = text } - - if (this$1.composing) { - this$1.composing.range.clear() - this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), - {className: "CodeMirror-composing"}) - } - }) - return true -}; - -TextareaInput.prototype.ensurePolled = function () { - if (this.pollingFast && this.poll()) { this.pollingFast = false } -}; - -TextareaInput.prototype.onKeyPress = function () { - if (ie && ie_version >= 9) { this.hasSelection = null } - this.fastPoll() -}; - -TextareaInput.prototype.onContextMenu = function (e) { - var input = this, cm = input.cm, display = cm.display, te = input.textarea - var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop - if (!pos || presto) { return } // Opera is difficult. - - // Reset the current text selection only if the click is done outside of the selection - // and 'resetSelectionOnContextMenu' option is true. - var reset = cm.options.resetSelectionOnContextMenu - if (reset && cm.doc.sel.contains(pos) == -1) - { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll) } - - var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText - input.wrapper.style.cssText = "position: absolute" - var wrapperBox = input.wrapper.getBoundingClientRect() - te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);" - var oldScrollY - if (webkit) { oldScrollY = window.scrollY } // Work around Chrome issue (#2712) - display.input.focus() - if (webkit) { window.scrollTo(null, oldScrollY) } - display.input.reset() - // Adds "Select all" to context menu in FF - if (!cm.somethingSelected()) { te.value = input.prevInput = " " } - input.contextMenuPending = true - display.selForContextMenu = cm.doc.sel - clearTimeout(display.detectingSelectAll) - - // Select-all will be greyed out if there's nothing to select, so - // this adds a zero-width space so that we can later check whether - // it got selected. - function prepareSelectAllHack() { - if (te.selectionStart != null) { - var selected = cm.somethingSelected() - var extval = "\u200b" + (selected ? te.value : "") - te.value = "\u21da" // Used to catch context-menu undo - te.value = extval - input.prevInput = selected ? "" : "\u200b" - te.selectionStart = 1; te.selectionEnd = extval.length - // Re-set this, in case some other handler touched the - // selection in the meantime. - display.selForContextMenu = cm.doc.sel - } - } - function rehide() { - input.contextMenuPending = false - input.wrapper.style.cssText = oldWrapperCSS - te.style.cssText = oldCSS - if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos) } - - // Try to detect the user choosing select-all - if (te.selectionStart != null) { - if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack() } - var i = 0, poll = function () { - if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && - te.selectionEnd > 0 && input.prevInput == "\u200b") { - operation(cm, selectAll)(cm) - } else if (i++ < 10) { - display.detectingSelectAll = setTimeout(poll, 500) - } else { - display.selForContextMenu = null - display.input.reset() - } - } - display.detectingSelectAll = setTimeout(poll, 200) - } - } - - if (ie && ie_version >= 9) { prepareSelectAllHack() } - if (captureRightClick) { - e_stop(e) - var mouseup = function () { - off(window, "mouseup", mouseup) - setTimeout(rehide, 20) - } - on(window, "mouseup", mouseup) - } else { - setTimeout(rehide, 50) - } -}; - -TextareaInput.prototype.readOnlyChanged = function (val) { - if (!val) { this.reset() } - this.textarea.disabled = val == "nocursor" -}; - -TextareaInput.prototype.setUneditable = function () {}; - -TextareaInput.prototype.needsContentAttribute = false - -function fromTextArea(textarea, options) { - options = options ? copyObj(options) : {} - options.value = textarea.value - if (!options.tabindex && textarea.tabIndex) - { options.tabindex = textarea.tabIndex } - if (!options.placeholder && textarea.placeholder) - { options.placeholder = textarea.placeholder } - // Set autofocus to true if this textarea is focused, or if it has - // autofocus and no other element is focused. - if (options.autofocus == null) { - var hasFocus = activeElt() - options.autofocus = hasFocus == textarea || - textarea.getAttribute("autofocus") != null && hasFocus == document.body - } - - function save() {textarea.value = cm.getValue()} - - var realSubmit - if (textarea.form) { - on(textarea.form, "submit", save) - // Deplorable hack to make the submit method do the right thing. - if (!options.leaveSubmitMethodAlone) { - var form = textarea.form - realSubmit = form.submit - try { - var wrappedSubmit = form.submit = function () { - save() - form.submit = realSubmit - form.submit() - form.submit = wrappedSubmit - } - } catch(e) {} - } - } - - options.finishInit = function (cm) { - cm.save = save - cm.getTextArea = function () { return textarea; } - cm.toTextArea = function () { - cm.toTextArea = isNaN // Prevent this from being ran twice - save() - textarea.parentNode.removeChild(cm.getWrapperElement()) - textarea.style.display = "" - if (textarea.form) { - off(textarea.form, "submit", save) - if (typeof textarea.form.submit == "function") - { textarea.form.submit = realSubmit } - } - } - } - - textarea.style.display = "none" - var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, - options) - return cm -} - -function addLegacyProps(CodeMirror) { - CodeMirror.off = off - CodeMirror.on = on - CodeMirror.wheelEventPixels = wheelEventPixels - CodeMirror.Doc = Doc - CodeMirror.splitLines = splitLinesAuto - CodeMirror.countColumn = countColumn - CodeMirror.findColumn = findColumn - CodeMirror.isWordChar = isWordCharBasic - CodeMirror.Pass = Pass - CodeMirror.signal = signal - CodeMirror.Line = Line - CodeMirror.changeEnd = changeEnd - CodeMirror.scrollbarModel = scrollbarModel - CodeMirror.Pos = Pos - CodeMirror.cmpPos = cmp - CodeMirror.modes = modes - CodeMirror.mimeModes = mimeModes - CodeMirror.resolveMode = resolveMode - CodeMirror.getMode = getMode - CodeMirror.modeExtensions = modeExtensions - CodeMirror.extendMode = extendMode - CodeMirror.copyState = copyState - CodeMirror.startState = startState - CodeMirror.innerMode = innerMode - CodeMirror.commands = commands - CodeMirror.keyMap = keyMap - CodeMirror.keyName = keyName - CodeMirror.isModifierKey = isModifierKey - CodeMirror.lookupKey = lookupKey - CodeMirror.normalizeKeyMap = normalizeKeyMap - CodeMirror.StringStream = StringStream - CodeMirror.SharedTextMarker = SharedTextMarker - CodeMirror.TextMarker = TextMarker - CodeMirror.LineWidget = LineWidget - CodeMirror.e_preventDefault = e_preventDefault - CodeMirror.e_stopPropagation = e_stopPropagation - CodeMirror.e_stop = e_stop - CodeMirror.addClass = addClass - CodeMirror.contains = contains - CodeMirror.rmClass = rmClass - CodeMirror.keyNames = keyNames -} - -// EDITOR CONSTRUCTOR - -defineOptions(CodeMirror) - -addEditorMethods(CodeMirror) - -// Set up methods on CodeMirror's prototype to redirect to the editor's document. -var dontDelegate = "iter insert remove copy getEditor constructor".split(" ") -for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) - { CodeMirror.prototype[prop] = (function(method) { - return function() {return method.apply(this.doc, arguments)} - })(Doc.prototype[prop]) } } - -eventMixin(Doc) - -// INPUT HANDLING - -CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput} - -// MODE DEFINITION AND QUERYING - -// Extra arguments are stored as the mode's dependencies, which is -// used by (legacy) mechanisms like loadmode.js to automatically -// load a mode. (Preferred mechanism is the require/define calls.) -CodeMirror.defineMode = function(name/*, mode, …*/) { - if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name } - defineMode.apply(this, arguments) -} - -CodeMirror.defineMIME = defineMIME - -// Minimal default mode. -CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }) -CodeMirror.defineMIME("text/plain", "null") - -// EXTENSIONS - -CodeMirror.defineExtension = function (name, func) { - CodeMirror.prototype[name] = func -} -CodeMirror.defineDocExtension = function (name, func) { - Doc.prototype[name] = func -} - -CodeMirror.fromTextArea = fromTextArea - -addLegacyProps(CodeMirror) - -CodeMirror.version = "5.28.0" - -return CodeMirror; - -}))); \ No newline at end of file diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/css.js b/js/DevHelper/Lib/CodeMirror/mode/css/css.js deleted file mode 100644 index 056c48e..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/css.js +++ /dev/null @@ -1,830 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("css", function(config, parserConfig) { - var inline = parserConfig.inline - if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); - - var indentUnit = config.indentUnit, - tokenHooks = parserConfig.tokenHooks, - documentTypes = parserConfig.documentTypes || {}, - mediaTypes = parserConfig.mediaTypes || {}, - mediaFeatures = parserConfig.mediaFeatures || {}, - mediaValueKeywords = parserConfig.mediaValueKeywords || {}, - propertyKeywords = parserConfig.propertyKeywords || {}, - nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, - fontProperties = parserConfig.fontProperties || {}, - counterDescriptors = parserConfig.counterDescriptors || {}, - colorKeywords = parserConfig.colorKeywords || {}, - valueKeywords = parserConfig.valueKeywords || {}, - allowNested = parserConfig.allowNested, - lineComment = parserConfig.lineComment, - supportsAtComponent = parserConfig.supportsAtComponent === true; - - var type, override; - function ret(style, tp) { type = tp; return style; } - - // Tokenizers - - function tokenBase(stream, state) { - var ch = stream.next(); - if (tokenHooks[ch]) { - var result = tokenHooks[ch](stream, state); - if (result !== false) return result; - } - if (ch == "@") { - stream.eatWhile(/[\w\\\-]/); - return ret("def", stream.current()); - } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { - return ret(null, "compare"); - } else if (ch == "\"" || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "#") { - stream.eatWhile(/[\w\\\-]/); - return ret("atom", "hash"); - } else if (ch == "!") { - stream.match(/^\s*\w*/); - return ret("keyword", "important"); - } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (ch === "-") { - if (/[\d.]/.test(stream.peek())) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (stream.match(/^-[\w\\\-]+/)) { - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ret("variable-2", "variable-definition"); - return ret("variable-2", "variable"); - } else if (stream.match(/^\w+-/)) { - return ret("meta", "meta"); - } - } else if (/[,+>*\/]/.test(ch)) { - return ret(null, "select-op"); - } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { - return ret("qualifier", "qualifier"); - } else if (/[:;{}\[\]\(\)]/.test(ch)) { - return ret(null, ch); - } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) || - (ch == "d" && stream.match("omain(")) || - (ch == "r" && stream.match("egexp("))) { - stream.backUp(1); - state.tokenize = tokenParenthesized; - return ret("property", "word"); - } else if (/[\w\\\-]/.test(ch)) { - stream.eatWhile(/[\w\\\-]/); - return ret("property", "word"); - } else { - return ret(null, null); - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, ch; - while ((ch = stream.next()) != null) { - if (ch == quote && !escaped) { - if (quote == ")") stream.backUp(1); - break; - } - escaped = !escaped && ch == "\\"; - } - if (ch == quote || !escaped && quote != ")") state.tokenize = null; - return ret("string", "string"); - }; - } - - function tokenParenthesized(stream, state) { - stream.next(); // Must be '(' - if (!stream.match(/\s*[\"\')]/, false)) - state.tokenize = tokenString(")"); - else - state.tokenize = null; - return ret(null, "("); - } - - // Context management - - function Context(type, indent, prev) { - this.type = type; - this.indent = indent; - this.prev = prev; - } - - function pushContext(state, stream, type, indent) { - state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context); - return type; - } - - function popContext(state) { - if (state.context.prev) - state.context = state.context.prev; - return state.context.type; - } - - function pass(type, stream, state) { - return states[state.context.type](type, stream, state); - } - function popAndPass(type, stream, state, n) { - for (var i = n || 1; i > 0; i--) - state.context = state.context.prev; - return pass(type, stream, state); - } - - // Parser - - function wordAsValue(stream) { - var word = stream.current().toLowerCase(); - if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "variable"; - } - - var states = {}; - - states.top = function(type, stream, state) { - if (type == "{") { - return pushContext(state, stream, "block"); - } else if (type == "}" && state.context.prev) { - return popContext(state); - } else if (supportsAtComponent && /@component/.test(type)) { - return pushContext(state, stream, "atComponentBlock"); - } else if (/^@(-moz-)?document$/.test(type)) { - return pushContext(state, stream, "documentTypes"); - } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) { - return pushContext(state, stream, "atBlock"); - } else if (/^@(font-face|counter-style)/.test(type)) { - state.stateArg = type; - return "restricted_atBlock_before"; - } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) { - return "keyframes"; - } else if (type && type.charAt(0) == "@") { - return pushContext(state, stream, "at"); - } else if (type == "hash") { - override = "builtin"; - } else if (type == "word") { - override = "tag"; - } else if (type == "variable-definition") { - return "maybeprop"; - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } else if (type == ":") { - return "pseudo"; - } else if (allowNested && type == "(") { - return pushContext(state, stream, "parens"); - } - return state.context.type; - }; - - states.block = function(type, stream, state) { - if (type == "word") { - var word = stream.current().toLowerCase(); - if (propertyKeywords.hasOwnProperty(word)) { - override = "property"; - return "maybeprop"; - } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) { - override = "string-2"; - return "maybeprop"; - } else if (allowNested) { - override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag"; - return "block"; - } else { - override += " error"; - return "maybeprop"; - } - } else if (type == "meta") { - return "block"; - } else if (!allowNested && (type == "hash" || type == "qualifier")) { - override = "error"; - return "block"; - } else { - return states.top(type, stream, state); - } - }; - - states.maybeprop = function(type, stream, state) { - if (type == ":") return pushContext(state, stream, "prop"); - return pass(type, stream, state); - }; - - states.prop = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); - if (type == "}" || type == "{") return popAndPass(type, stream, state); - if (type == "(") return pushContext(state, stream, "parens"); - - if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) { - override += " error"; - } else if (type == "word") { - wordAsValue(stream); - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } - return "prop"; - }; - - states.propBlock = function(type, _stream, state) { - if (type == "}") return popContext(state); - if (type == "word") { override = "property"; return "maybeprop"; } - return state.context.type; - }; - - states.parens = function(type, stream, state) { - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == ")") return popContext(state); - if (type == "(") return pushContext(state, stream, "parens"); - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - if (type == "word") wordAsValue(stream); - return "parens"; - }; - - states.pseudo = function(type, stream, state) { - if (type == "meta") return "pseudo"; - - if (type == "word") { - override = "variable-3"; - return state.context.type; - } - return pass(type, stream, state); - }; - - states.documentTypes = function(type, stream, state) { - if (type == "word" && documentTypes.hasOwnProperty(stream.current())) { - override = "tag"; - return state.context.type; - } else { - return states.atBlock(type, stream, state); - } - }; - - states.atBlock = function(type, stream, state) { - if (type == "(") return pushContext(state, stream, "atBlock_parens"); - if (type == "}" || type == ";") return popAndPass(type, stream, state); - if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); - - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - - if (type == "word") { - var word = stream.current().toLowerCase(); - if (word == "only" || word == "not" || word == "and" || word == "or") - override = "keyword"; - else if (mediaTypes.hasOwnProperty(word)) - override = "attribute"; - else if (mediaFeatures.hasOwnProperty(word)) - override = "property"; - else if (mediaValueKeywords.hasOwnProperty(word)) - override = "keyword"; - else if (propertyKeywords.hasOwnProperty(word)) - override = "property"; - else if (nonStandardPropertyKeywords.hasOwnProperty(word)) - override = "string-2"; - else if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "error"; - } - return state.context.type; - }; - - states.atComponentBlock = function(type, stream, state) { - if (type == "}") - return popAndPass(type, stream, state); - if (type == "{") - return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false); - if (type == "word") - override = "error"; - return state.context.type; - }; - - states.atBlock_parens = function(type, stream, state) { - if (type == ")") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); - return states.atBlock(type, stream, state); - }; - - states.restricted_atBlock_before = function(type, stream, state) { - if (type == "{") - return pushContext(state, stream, "restricted_atBlock"); - if (type == "word" && state.stateArg == "@counter-style") { - override = "variable"; - return "restricted_atBlock_before"; - } - return pass(type, stream, state); - }; - - states.restricted_atBlock = function(type, stream, state) { - if (type == "}") { - state.stateArg = null; - return popContext(state); - } - if (type == "word") { - if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) || - (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase()))) - override = "error"; - else - override = "property"; - return "maybeprop"; - } - return "restricted_atBlock"; - }; - - states.keyframes = function(type, stream, state) { - if (type == "word") { override = "variable"; return "keyframes"; } - if (type == "{") return pushContext(state, stream, "top"); - return pass(type, stream, state); - }; - - states.at = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == "word") override = "tag"; - else if (type == "hash") override = "builtin"; - return "at"; - }; - - states.interpolation = function(type, stream, state) { - if (type == "}") return popContext(state); - if (type == "{" || type == ";") return popAndPass(type, stream, state); - if (type == "word") override = "variable"; - else if (type != "variable" && type != "(" && type != ")") override = "error"; - return "interpolation"; - }; - - return { - startState: function(base) { - return {tokenize: null, - state: inline ? "block" : "top", - stateArg: null, - context: new Context(inline ? "block" : "top", base || 0, null)}; - }, - - token: function(stream, state) { - if (!state.tokenize && stream.eatSpace()) return null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style && typeof style == "object") { - type = style[1]; - style = style[0]; - } - override = style; - state.state = states[state.state](type, stream, state); - return override; - }, - - indent: function(state, textAfter) { - var cx = state.context, ch = textAfter && textAfter.charAt(0); - var indent = cx.indent; - if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; - if (cx.prev) { - if (ch == "}" && (cx.type == "block" || cx.type == "top" || - cx.type == "interpolation" || cx.type == "restricted_atBlock")) { - // Resume indentation from parent context. - cx = cx.prev; - indent = cx.indent; - } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || - ch == "{" && (cx.type == "at" || cx.type == "atBlock")) { - // Dedent relative to current context. - indent = Math.max(0, cx.indent - indentUnit); - } - } - return indent; - }, - - electricChars: "}", - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: lineComment, - fold: "brace" - }; -}); - - function keySet(array) { - var keys = {}; - for (var i = 0; i < array.length; ++i) { - keys[array[i].toLowerCase()] = true; - } - return keys; - } - - var documentTypes_ = [ - "domain", "regexp", "url", "url-prefix" - ], documentTypes = keySet(documentTypes_); - - var mediaTypes_ = [ - "all", "aural", "braille", "handheld", "print", "projection", "screen", - "tty", "tv", "embossed" - ], mediaTypes = keySet(mediaTypes_); - - var mediaFeatures_ = [ - "width", "min-width", "max-width", "height", "min-height", "max-height", - "device-width", "min-device-width", "max-device-width", "device-height", - "min-device-height", "max-device-height", "aspect-ratio", - "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", - "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", - "max-color", "color-index", "min-color-index", "max-color-index", - "monochrome", "min-monochrome", "max-monochrome", "resolution", - "min-resolution", "max-resolution", "scan", "grid", "orientation", - "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", - "pointer", "any-pointer", "hover", "any-hover" - ], mediaFeatures = keySet(mediaFeatures_); - - var mediaValueKeywords_ = [ - "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", - "interlace", "progressive" - ], mediaValueKeywords = keySet(mediaValueKeywords_); - - var propertyKeywords_ = [ - "align-content", "align-items", "align-self", "alignment-adjust", - "alignment-baseline", "anchor-point", "animation", "animation-delay", - "animation-direction", "animation-duration", "animation-fill-mode", - "animation-iteration-count", "animation-name", "animation-play-state", - "animation-timing-function", "appearance", "azimuth", "backface-visibility", - "background", "background-attachment", "background-blend-mode", "background-clip", - "background-color", "background-image", "background-origin", "background-position", - "background-repeat", "background-size", "baseline-shift", "binding", - "bleed", "bookmark-label", "bookmark-level", "bookmark-state", - "bookmark-target", "border", "border-bottom", "border-bottom-color", - "border-bottom-left-radius", "border-bottom-right-radius", - "border-bottom-style", "border-bottom-width", "border-collapse", - "border-color", "border-image", "border-image-outset", - "border-image-repeat", "border-image-slice", "border-image-source", - "border-image-width", "border-left", "border-left-color", - "border-left-style", "border-left-width", "border-radius", "border-right", - "border-right-color", "border-right-style", "border-right-width", - "border-spacing", "border-style", "border-top", "border-top-color", - "border-top-left-radius", "border-top-right-radius", "border-top-style", - "border-top-width", "border-width", "bottom", "box-decoration-break", - "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", - "caption-side", "caret-color", "clear", "clip", "color", "color-profile", "column-count", - "column-fill", "column-gap", "column-rule", "column-rule-color", - "column-rule-style", "column-rule-width", "column-span", "column-width", - "columns", "content", "counter-increment", "counter-reset", "crop", "cue", - "cue-after", "cue-before", "cursor", "direction", "display", - "dominant-baseline", "drop-initial-after-adjust", - "drop-initial-after-align", "drop-initial-before-adjust", - "drop-initial-before-align", "drop-initial-size", "drop-initial-value", - "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis", - "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", - "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings", - "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust", - "font-stretch", "font-style", "font-synthesis", "font-variant", - "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", - "font-variant-ligatures", "font-variant-numeric", "font-variant-position", - "font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", - "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap", - "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap", - "grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns", - "grid-template-rows", "hanging-punctuation", "height", "hyphens", - "icon", "image-orientation", "image-rendering", "image-resolution", - "inline-box-align", "justify-content", "justify-items", "justify-self", "left", "letter-spacing", - "line-break", "line-height", "line-stacking", "line-stacking-ruby", - "line-stacking-shift", "line-stacking-strategy", "list-style", - "list-style-image", "list-style-position", "list-style-type", "margin", - "margin-bottom", "margin-left", "margin-right", "margin-top", - "marks", "marquee-direction", "marquee-loop", - "marquee-play-count", "marquee-speed", "marquee-style", "max-height", - "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", - "nav-left", "nav-right", "nav-up", "object-fit", "object-position", - "opacity", "order", "orphans", "outline", - "outline-color", "outline-offset", "outline-style", "outline-width", - "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y", - "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", - "page", "page-break-after", "page-break-before", "page-break-inside", - "page-policy", "pause", "pause-after", "pause-before", "perspective", - "perspective-origin", "pitch", "pitch-range", "place-content", "place-items", "place-self", "play-during", "position", - "presentation-level", "punctuation-trim", "quotes", "region-break-after", - "region-break-before", "region-break-inside", "region-fragment", - "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness", - "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", - "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin", - "shape-outside", "size", "speak", "speak-as", "speak-header", - "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", - "tab-size", "table-layout", "target", "target-name", "target-new", - "target-position", "text-align", "text-align-last", "text-decoration", - "text-decoration-color", "text-decoration-line", "text-decoration-skip", - "text-decoration-style", "text-emphasis", "text-emphasis-color", - "text-emphasis-position", "text-emphasis-style", "text-height", - "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow", - "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position", - "text-wrap", "top", "transform", "transform-origin", "transform-style", - "transition", "transition-delay", "transition-duration", - "transition-property", "transition-timing-function", "unicode-bidi", - "user-select", "vertical-align", "visibility", "voice-balance", "voice-duration", - "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", - "voice-volume", "volume", "white-space", "widows", "width", "will-change", "word-break", - "word-spacing", "word-wrap", "z-index", - // SVG-specific - "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", - "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", - "color-interpolation", "color-interpolation-filters", - "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", - "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", - "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", - "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", - "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", - "glyph-orientation-vertical", "text-anchor", "writing-mode" - ], propertyKeywords = keySet(propertyKeywords_); - - var nonStandardPropertyKeywords_ = [ - "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color", - "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", - "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside", - "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button", - "searchfield-results-decoration", "zoom" - ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_); - - var fontProperties_ = [ - "font-family", "src", "unicode-range", "font-variant", "font-feature-settings", - "font-stretch", "font-weight", "font-style" - ], fontProperties = keySet(fontProperties_); - - var counterDescriptors_ = [ - "additive-symbols", "fallback", "negative", "pad", "prefix", "range", - "speak-as", "suffix", "symbols", "system" - ], counterDescriptors = keySet(counterDescriptors_); - - var colorKeywords_ = [ - "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", - "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", - "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", - "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", - "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", - "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", - "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", - "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", - "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", - "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", - "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", - "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", - "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", - "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", - "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", - "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", - "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", - "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", - "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", - "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", - "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", - "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", - "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", - "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", - "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", - "whitesmoke", "yellow", "yellowgreen" - ], colorKeywords = keySet(colorKeywords_); - - var valueKeywords_ = [ - "above", "absolute", "activeborder", "additive", "activecaption", "afar", - "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", - "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", - "arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page", - "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", - "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", - "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel", - "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", - "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", - "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", - "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", - "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse", - "compact", "condensed", "contain", "content", "contents", - "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop", - "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal", - "decimal-leading-zero", "default", "default-button", "dense", "destination-atop", - "destination-in", "destination-out", "destination-over", "devanagari", "difference", - "disc", "discard", "disclosure-closed", "disclosure-open", "document", - "dot-dash", "dot-dot-dash", - "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", - "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", - "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", - "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", - "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", - "ethiopic-halehame-gez", "ethiopic-halehame-om-et", - "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", - "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", - "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", - "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", - "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove", - "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", - "help", "hidden", "hide", "higher", "highlight", "highlighttext", - "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore", - "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", - "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", - "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert", - "italic", "japanese-formal", "japanese-informal", "justify", "kannada", - "katakana", "katakana-iroha", "keep-all", "khmer", - "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", - "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten", - "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", - "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", - "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", - "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d", - "media-controls-background", "media-current-time-display", - "media-fullscreen-button", "media-mute-button", "media-play-button", - "media-return-to-realtime-button", "media-rewind-button", - "media-seek-back-button", "media-seek-forward-button", "media-slider", - "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", - "media-volume-slider-container", "media-volume-sliderthumb", "medium", - "menu", "menulist", "menulist-button", "menulist-text", - "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", - "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize", - "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", - "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", - "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote", - "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", - "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", - "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter", - "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", - "progress", "push-button", "radial-gradient", "radio", "read-only", - "read-write", "read-write-plaintext-only", "rectangle", "region", - "relative", "repeat", "repeating-linear-gradient", - "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse", - "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", - "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running", - "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen", - "scroll", "scrollbar", "scroll-position", "se-resize", "searchfield", - "searchfield-cancel-button", "searchfield-decoration", - "searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end", - "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", - "simp-chinese-formal", "simp-chinese-informal", "single", - "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", - "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", - "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", - "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square", - "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", - "table-caption", "table-cell", "table-column", "table-column-group", - "table-footer-group", "table-header-group", "table-row", "table-row-group", - "tamil", - "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", - "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", - "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", - "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", - "trad-chinese-formal", "trad-chinese-informal", "transform", - "translate", "translate3d", "translateX", "translateY", "translateZ", - "transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up", - "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", - "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", - "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", - "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", - "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", - "xx-large", "xx-small" - ], valueKeywords = keySet(valueKeywords_); - - var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_) - .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_) - .concat(valueKeywords_); - CodeMirror.registerHelper("hintWords", "css", allWords); - - function tokenCComment(stream, state) { - var maybeEnd = false, ch; - while ((ch = stream.next()) != null) { - if (maybeEnd && ch == "/") { - state.tokenize = null; - break; - } - maybeEnd = (ch == "*"); - } - return ["comment", "comment"]; - } - - CodeMirror.defineMIME("text/css", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css" - }); - - CodeMirror.defineMIME("text/x-scss", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - ":": function(stream) { - if (stream.match(/\s*\{/, false)) - return [null, null] - return false; - }, - "$": function(stream) { - stream.match(/^[\w-]+/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "#": function(stream) { - if (!stream.eat("{")) return false; - return [null, "interpolation"]; - } - }, - name: "css", - helperType: "scss" - }); - - CodeMirror.defineMIME("text/x-less", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - "@": function(stream) { - if (stream.eat("{")) return [null, "interpolation"]; - if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false; - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "&": function() { - return ["atom", "atom"]; - } - }, - name: "css", - helperType: "less" - }); - - CodeMirror.defineMIME("text/x-gss", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - supportsAtComponent: true, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css", - helperType: "gss" - }); - -}); diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/gss.html b/js/DevHelper/Lib/CodeMirror/mode/css/gss.html deleted file mode 100644 index 232fe8c..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/gss.html +++ /dev/null @@ -1,103 +0,0 @@ - - -CodeMirror: Closure Stylesheets (GSS) mode - - - - - - - - - - - - -
-

Closure Stylesheets (GSS) mode

-
- - -

A mode for Closure Stylesheets (GSS).

-

MIME type defined: text/x-gss.

- -

Parsing/Highlighting Tests: normal, verbose.

- -
diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/gss_test.js b/js/DevHelper/Lib/CodeMirror/mode/css/gss_test.js deleted file mode 100644 index d56e592..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/gss_test.js +++ /dev/null @@ -1,17 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - "use strict"; - - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-gss"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "gss"); } - - MT("atComponent", - "[def @component] {", - "[tag foo] {", - " [property color]: [keyword black];", - "}", - "}"); - -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/index.html b/js/DevHelper/Lib/CodeMirror/mode/css/index.html deleted file mode 100644 index 0d85311..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - -CodeMirror: CSS mode - - - - - - - - - - - - -
-

CSS mode

-
- - -

MIME types defined: text/css, text/x-scss (demo), text/x-less (demo).

- -

Parsing/Highlighting Tests: normal, verbose.

- -
diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/less.html b/js/DevHelper/Lib/CodeMirror/mode/css/less.html deleted file mode 100644 index adf7427..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/less.html +++ /dev/null @@ -1,152 +0,0 @@ - - -CodeMirror: LESS mode - - - - - - - - - - -
-

LESS mode

-
- - -

The LESS mode is a sub-mode of the CSS mode (defined in css.js).

- -

Parsing/Highlighting Tests: normal, verbose.

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/less_test.js b/js/DevHelper/Lib/CodeMirror/mode/css/less_test.js deleted file mode 100644 index dd82155..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/less_test.js +++ /dev/null @@ -1,54 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - "use strict"; - - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-less"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "less"); } - - MT("variable", - "[variable-2 @base]: [atom #f04615];", - "[qualifier .class] {", - " [property width]: [variable percentage]([number 0.5]); [comment // returns `50%`]", - " [property color]: [variable saturate]([variable-2 @base], [number 5%]);", - "}"); - - MT("amp", - "[qualifier .child], [qualifier .sibling] {", - " [qualifier .parent] [atom &] {", - " [property color]: [keyword black];", - " }", - " [atom &] + [atom &] {", - " [property color]: [keyword red];", - " }", - "}"); - - MT("mixin", - "[qualifier .mixin] ([variable dark]; [variable-2 @color]) {", - " [property color]: [atom darken]([variable-2 @color], [number 10%]);", - "}", - "[qualifier .mixin] ([variable light]; [variable-2 @color]) {", - " [property color]: [atom lighten]([variable-2 @color], [number 10%]);", - "}", - "[qualifier .mixin] ([variable-2 @_]; [variable-2 @color]) {", - " [property display]: [atom block];", - "}", - "[variable-2 @switch]: [variable light];", - "[qualifier .class] {", - " [qualifier .mixin]([variable-2 @switch]; [atom #888]);", - "}"); - - MT("nest", - "[qualifier .one] {", - " [def @media] ([property width]: [number 400px]) {", - " [property font-size]: [number 1.2em];", - " [def @media] [attribute print] [keyword and] [property color] {", - " [property color]: [keyword blue];", - " }", - " }", - "}"); - - - MT("interpolation", ".@{[variable foo]} { [property font-weight]: [atom bold]; }"); -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/scss.html b/js/DevHelper/Lib/CodeMirror/mode/css/scss.html deleted file mode 100644 index f8e4d37..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/scss.html +++ /dev/null @@ -1,157 +0,0 @@ - - -CodeMirror: SCSS mode - - - - - - - - - -
-

SCSS mode

-
- - -

The SCSS mode is a sub-mode of the CSS mode (defined in css.js).

- -

Parsing/Highlighting Tests: normal, verbose.

- -
diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/scss_test.js b/js/DevHelper/Lib/CodeMirror/mode/css/scss_test.js deleted file mode 100644 index 785921b..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/scss_test.js +++ /dev/null @@ -1,110 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-scss"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } - - MT('url_with_quotation', - "[tag foo] { [property background]:[atom url]([string test.jpg]) }"); - - MT('url_with_double_quotes', - "[tag foo] { [property background]:[atom url]([string \"test.jpg\"]) }"); - - MT('url_with_single_quotes', - "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) }"); - - MT('string', - "[def @import] [string \"compass/css3\"]"); - - MT('important_keyword', - "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) [keyword !important] }"); - - MT('variable', - "[variable-2 $blue]:[atom #333]"); - - MT('variable_as_attribute', - "[tag foo] { [property color]:[variable-2 $blue] }"); - - MT('numbers', - "[tag foo] { [property padding]:[number 10px] [number 10] [number 10em] [number 8in] }"); - - MT('number_percentage', - "[tag foo] { [property width]:[number 80%] }"); - - MT('selector', - "[builtin #hello][qualifier .world]{}"); - - MT('singleline_comment', - "[comment // this is a comment]"); - - MT('multiline_comment', - "[comment /*foobar*/]"); - - MT('attribute_with_hyphen', - "[tag foo] { [property font-size]:[number 10px] }"); - - MT('string_after_attribute', - "[tag foo] { [property content]:[string \"::\"] }"); - - MT('directives', - "[def @include] [qualifier .mixin]"); - - MT('basic_structure', - "[tag p] { [property background]:[keyword red]; }"); - - MT('nested_structure', - "[tag p] { [tag a] { [property color]:[keyword red]; } }"); - - MT('mixin', - "[def @mixin] [tag table-base] {}"); - - MT('number_without_semicolon', - "[tag p] {[property width]:[number 12]}", - "[tag a] {[property color]:[keyword red];}"); - - MT('atom_in_nested_block', - "[tag p] { [tag a] { [property color]:[atom #000]; } }"); - - MT('interpolation_in_property', - "[tag foo] { #{[variable-2 $hello]}:[number 2]; }"); - - MT('interpolation_in_selector', - "[tag foo]#{[variable-2 $hello]} { [property color]:[atom #000]; }"); - - MT('interpolation_error', - "[tag foo]#{[variable foo]} { [property color]:[atom #000]; }"); - - MT("divide_operator", - "[tag foo] { [property width]:[number 4] [operator /] [number 2] }"); - - MT('nested_structure_with_id_selector', - "[tag p] { [builtin #hello] { [property color]:[keyword red]; } }"); - - MT('indent_mixin', - "[def @mixin] [tag container] (", - " [variable-2 $a]: [number 10],", - " [variable-2 $b]: [number 10])", - "{}"); - - MT('indent_nested', - "[tag foo] {", - " [tag bar] {", - " }", - "}"); - - MT('indent_parentheses', - "[tag foo] {", - " [property color]: [atom darken]([variable-2 $blue],", - " [number 9%]);", - "}"); - - MT('indent_vardef', - "[variable-2 $name]:", - " [string 'val'];", - "[tag tag] {", - " [tag inner] {", - " [property margin]: [number 3px];", - " }", - "}"); -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/css/test.js b/js/DevHelper/Lib/CodeMirror/mode/css/test.js deleted file mode 100644 index 7a496fb..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/css/test.js +++ /dev/null @@ -1,200 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - var mode = CodeMirror.getMode({indentUnit: 2}, "css"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } - - // Error, because "foobarhello" is neither a known type or property, but - // property was expected (after "and"), and it should be in parentheses. - MT("atMediaUnknownType", - "[def @media] [attribute screen] [keyword and] [error foobarhello] { }"); - - // Soft error, because "foobarhello" is not a known property or type. - MT("atMediaUnknownProperty", - "[def @media] [attribute screen] [keyword and] ([error foobarhello]) { }"); - - // Make sure nesting works with media queries - MT("atMediaMaxWidthNested", - "[def @media] [attribute screen] [keyword and] ([property max-width]: [number 25px]) { [tag foo] { } }"); - - MT("atMediaFeatureValueKeyword", - "[def @media] ([property orientation]: [keyword landscape]) { }"); - - MT("atMediaUnknownFeatureValueKeyword", - "[def @media] ([property orientation]: [error upsidedown]) { }"); - - MT("tagSelector", - "[tag foo] { }"); - - MT("classSelector", - "[qualifier .foo-bar_hello] { }"); - - MT("idSelector", - "[builtin #foo] { [error #foo] }"); - - MT("tagSelectorUnclosed", - "[tag foo] { [property margin]: [number 0] } [tag bar] { }"); - - MT("tagStringNoQuotes", - "[tag foo] { [property font-family]: [variable hello] [variable world]; }"); - - MT("tagStringDouble", - "[tag foo] { [property font-family]: [string \"hello world\"]; }"); - - MT("tagStringSingle", - "[tag foo] { [property font-family]: [string 'hello world']; }"); - - MT("tagColorKeyword", - "[tag foo] {", - " [property color]: [keyword black];", - " [property color]: [keyword navy];", - " [property color]: [keyword yellow];", - "}"); - - MT("tagColorHex3", - "[tag foo] { [property background]: [atom #fff]; }"); - - MT("tagColorHex4", - "[tag foo] { [property background]: [atom #ffff]; }"); - - MT("tagColorHex6", - "[tag foo] { [property background]: [atom #ffffff]; }"); - - MT("tagColorHex8", - "[tag foo] { [property background]: [atom #ffffffff]; }"); - - MT("tagColorHex5Invalid", - "[tag foo] { [property background]: [atom&error #fffff]; }"); - - MT("tagColorHexInvalid", - "[tag foo] { [property background]: [atom&error #ffg]; }"); - - MT("tagNegativeNumber", - "[tag foo] { [property margin]: [number -5px]; }"); - - MT("tagPositiveNumber", - "[tag foo] { [property padding]: [number 5px]; }"); - - MT("tagVendor", - "[tag foo] { [meta -foo-][property box-sizing]: [meta -foo-][atom border-box]; }"); - - MT("tagBogusProperty", - "[tag foo] { [property&error barhelloworld]: [number 0]; }"); - - MT("tagTwoProperties", - "[tag foo] { [property margin]: [number 0]; [property padding]: [number 0]; }"); - - MT("tagTwoPropertiesURL", - "[tag foo] { [property background]: [atom url]([string //example.com/foo.png]); [property padding]: [number 0]; }"); - - MT("indent_tagSelector", - "[tag strong], [tag em] {", - " [property background]: [atom rgba](", - " [number 255], [number 255], [number 0], [number .2]", - " );", - "}"); - - MT("indent_atMedia", - "[def @media] {", - " [tag foo] {", - " [property color]:", - " [keyword yellow];", - " }", - "}"); - - MT("indent_comma", - "[tag foo] {", - " [property font-family]: [variable verdana],", - " [atom sans-serif];", - "}"); - - MT("indent_parentheses", - "[tag foo]:[variable-3 before] {", - " [property background]: [atom url](", - "[string blahblah]", - "[string etc]", - "[string ]) [keyword !important];", - "}"); - - MT("font_face", - "[def @font-face] {", - " [property font-family]: [string 'myfont'];", - " [error nonsense]: [string 'abc'];", - " [property src]: [atom url]([string http://blah]),", - " [atom url]([string http://foo]);", - "}"); - - MT("empty_url", - "[def @import] [atom url]() [attribute screen];"); - - MT("parens", - "[qualifier .foo] {", - " [property background-image]: [variable fade]([atom #000], [number 20%]);", - " [property border-image]: [atom linear-gradient](", - " [atom to] [atom bottom],", - " [variable fade]([atom #000], [number 20%]) [number 0%],", - " [variable fade]([atom #000], [number 20%]) [number 100%]", - " );", - "}"); - - MT("css_variable", - ":[variable-3 root] {", - " [variable-2 --main-color]: [atom #06c];", - "}", - "[tag h1][builtin #foo] {", - " [property color]: [atom var]([variable-2 --main-color]);", - "}"); - - MT("supports", - "[def @supports] ([keyword not] (([property text-align-last]: [atom justify]) [keyword or] ([meta -moz-][property text-align-last]: [atom justify])) {", - " [property text-align-last]: [atom justify];", - "}"); - - MT("document", - "[def @document] [tag url]([string http://blah]),", - " [tag url-prefix]([string https://]),", - " [tag domain]([string blah.com]),", - " [tag regexp]([string \".*blah.+\"]) {", - " [builtin #id] {", - " [property background-color]: [keyword white];", - " }", - " [tag foo] {", - " [property font-family]: [variable Verdana], [atom sans-serif];", - " }", - "}"); - - MT("document_url", - "[def @document] [tag url]([string http://blah]) { [qualifier .class] { } }"); - - MT("document_urlPrefix", - "[def @document] [tag url-prefix]([string https://]) { [builtin #id] { } }"); - - MT("document_domain", - "[def @document] [tag domain]([string blah.com]) { [tag foo] { } }"); - - MT("document_regexp", - "[def @document] [tag regexp]([string \".*blah.+\"]) { [builtin #id] { } }"); - - MT("counter-style", - "[def @counter-style] [variable binary] {", - " [property system]: [atom numeric];", - " [property symbols]: [number 0] [number 1];", - " [property suffix]: [string \".\"];", - " [property range]: [atom infinite];", - " [property speak-as]: [atom numeric];", - "}"); - - MT("counter-style-additive-symbols", - "[def @counter-style] [variable simple-roman] {", - " [property system]: [atom additive];", - " [property additive-symbols]: [number 10] [variable X], [number 5] [variable V], [number 1] [variable I];", - " [property range]: [number 1] [number 49];", - "}"); - - MT("counter-style-use", - "[tag ol][qualifier .roman] { [property list-style]: [variable simple-roman]; }"); - - MT("counter-style-symbols", - "[tag ol] { [property list-style]: [atom symbols]([atom cyclic] [string \"*\"] [string \"\\2020\"] [string \"\\2021\"] [string \"\\A7\"]); }"); -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/htmlembedded.js b/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/htmlembedded.js deleted file mode 100644 index 464dc57..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/htmlembedded.js +++ /dev/null @@ -1,28 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), - require("../../addon/mode/multiplex")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../htmlmixed/htmlmixed", - "../../addon/mode/multiplex"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { - return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "htmlmixed"), { - open: parserConfig.open || parserConfig.scriptStartRegex || "<%", - close: parserConfig.close || parserConfig.scriptEndRegex || "%>", - mode: CodeMirror.getMode(config, parserConfig.scriptingModeSpec) - }); - }, "htmlmixed"); - - CodeMirror.defineMIME("application/x-ejs", {name: "htmlembedded", scriptingModeSpec:"javascript"}); - CodeMirror.defineMIME("application/x-aspx", {name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); - CodeMirror.defineMIME("application/x-jsp", {name: "htmlembedded", scriptingModeSpec:"text/x-java"}); - CodeMirror.defineMIME("application/x-erb", {name: "htmlembedded", scriptingModeSpec:"ruby"}); -}); diff --git a/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/index.html b/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/index.html deleted file mode 100644 index 9ed33cf..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/htmlembedded/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - -CodeMirror: Html Embedded Scripts mode - - - - - - - - - - - - - - -
-

Html Embedded Scripts mode

-
- - - -

Mode for html embedded scripts like JSP and ASP.NET. Depends on multiplex and HtmlMixed which in turn depends on - JavaScript, CSS and XML.
Other dependencies include those of the scripting language chosen.

- -

MIME types defined: application/x-aspx (ASP.NET), - application/x-ejs (Embedded Javascript), application/x-jsp (JavaServer Pages) - and application/x-erb

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/htmlmixed.js b/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/htmlmixed.js deleted file mode 100644 index 33398ec..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/htmlmixed.js +++ /dev/null @@ -1,152 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaultTags = { - script: [ - ["lang", /(javascript|babel)/i, "javascript"], - ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"], - ["type", /./, "text/plain"], - [null, null, "javascript"] - ], - style: [ - ["lang", /^css$/i, "css"], - ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"], - ["type", /./, "text/plain"], - [null, null, "css"] - ] - }; - - function maybeBackup(stream, pat, style) { - var cur = stream.current(), close = cur.search(pat); - if (close > -1) { - stream.backUp(cur.length - close); - } else if (cur.match(/<\/?$/)) { - stream.backUp(cur.length); - if (!stream.match(pat, false)) stream.match(cur); - } - return style; - } - - var attrRegexpCache = {}; - function getAttrRegexp(attr) { - var regexp = attrRegexpCache[attr]; - if (regexp) return regexp; - return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"); - } - - function getAttrValue(text, attr) { - var match = text.match(getAttrRegexp(attr)) - return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : "" - } - - function getTagRegexp(tagName, anchored) { - return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i"); - } - - function addTags(from, to) { - for (var tag in from) { - var dest = to[tag] || (to[tag] = []); - var source = from[tag]; - for (var i = source.length - 1; i >= 0; i--) - dest.unshift(source[i]) - } - } - - function findMatchingMode(tagInfo, tagText) { - for (var i = 0; i < tagInfo.length; i++) { - var spec = tagInfo[i]; - if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2]; - } - } - - CodeMirror.defineMode("htmlmixed", function (config, parserConfig) { - var htmlMode = CodeMirror.getMode(config, { - name: "xml", - htmlMode: true, - multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, - multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag - }); - - var tags = {}; - var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes; - addTags(defaultTags, tags); - if (configTags) addTags(configTags, tags); - if (configScript) for (var i = configScript.length - 1; i >= 0; i--) - tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]) - - function html(stream, state) { - var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName - if (tag && !/[<>\s\/]/.test(stream.current()) && - (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) && - tags.hasOwnProperty(tagName)) { - state.inTag = tagName + " " - } else if (state.inTag && tag && />$/.test(stream.current())) { - var inTag = /^([\S]+) (.*)/.exec(state.inTag) - state.inTag = null - var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2]) - var mode = CodeMirror.getMode(config, modeSpec) - var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false); - state.token = function (stream, state) { - if (stream.match(endTagA, false)) { - state.token = html; - state.localState = state.localMode = null; - return null; - } - return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState)); - }; - state.localMode = mode; - state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "")); - } else if (state.inTag) { - state.inTag += stream.current() - if (stream.eol()) state.inTag += " " - } - return style; - }; - - return { - startState: function () { - var state = CodeMirror.startState(htmlMode); - return {token: html, inTag: null, localMode: null, localState: null, htmlState: state}; - }, - - copyState: function (state) { - var local; - if (state.localState) { - local = CodeMirror.copyState(state.localMode, state.localState); - } - return {token: state.token, inTag: state.inTag, - localMode: state.localMode, localState: local, - htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; - }, - - token: function (stream, state) { - return state.token(stream, state); - }, - - indent: function (state, textAfter, line) { - if (!state.localMode || /^\s*<\//.test(textAfter)) - return htmlMode.indent(state.htmlState, textAfter); - else if (state.localMode.indent) - return state.localMode.indent(state.localState, textAfter, line); - else - return CodeMirror.Pass; - }, - - innerMode: function (state) { - return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; - } - }; - }, "xml", "javascript", "css"); - - CodeMirror.defineMIME("text/html", "htmlmixed"); -}); diff --git a/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/index.html b/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/index.html deleted file mode 100644 index caa7546..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/htmlmixed/index.html +++ /dev/null @@ -1,100 +0,0 @@ - - -CodeMirror: HTML mixed mode - - - - - - - - - - - - - - -
-

HTML mixed mode

-
- - -

The HTML mixed mode depends on the XML, JavaScript, and CSS modes.

- -

It takes an optional mode configuration - option, tags, which can be used to add custom - behavior for specific tags. When given, it should be an object - mapping tag names (for example script) to arrays or - three-element arrays. Those inner arrays indicate [attributeName, - valueRegexp, modeSpec] - specifications. For example, you could use ["type", /^foo$/, - "foo"] to map the attribute type="foo" to - the foo mode. When the first two fields are null - ([null, null, "mode"]), the given mode is used for - any such tag that doesn't match any of the previously given - attributes. For example:

- -
var myModeSpec = {
-  name: "htmlmixed",
-  tags: {
-    style: [["type", /^text/(x-)?scss$/, "text/x-scss"],
-            [null, null, "css"]],
-    custom: [[null, null, "customMode"]]
-  }
-}
- -

MIME types defined: text/html - (redefined, only takes effect if you load this parser after the - XML parser).

- -
diff --git a/js/DevHelper/Lib/CodeMirror/mode/javascript/index.html b/js/DevHelper/Lib/CodeMirror/mode/javascript/index.html deleted file mode 100644 index 592a133..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/javascript/index.html +++ /dev/null @@ -1,114 +0,0 @@ - - -CodeMirror: JavaScript mode - - - - - - - - - - - - -
-

JavaScript mode

- - -
- - - -

- JavaScript mode supports several configuration options: -

-

- -

MIME types defined: text/javascript, application/json, application/ld+json, text/typescript, application/typescript.

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/javascript/javascript.js b/js/DevHelper/Lib/CodeMirror/mode/javascript/javascript.js deleted file mode 100644 index 692ffc6..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/javascript/javascript.js +++ /dev/null @@ -1,818 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -function expressionAllowed(stream, state, backUp) { - return /^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(state.lastType) || - (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) -} - -CodeMirror.defineMode("javascript", function(config, parserConfig) { - var indentUnit = config.indentUnit; - var statementIndent = parserConfig.statementIndent; - var jsonldMode = parserConfig.jsonld; - var jsonMode = parserConfig.json || jsonldMode; - var isTS = parserConfig.typescript; - var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; - - // Tokenizer - - var keywords = function(){ - function kw(type) {return {type: type, style: "keyword"};} - var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); - var operator = kw("operator"), atom = {type: "atom", style: "atom"}; - - var jsKeywords = { - "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, - "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C, - "var": kw("var"), "const": kw("var"), "let": kw("var"), - "function": kw("function"), "catch": kw("catch"), - "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), - "in": operator, "typeof": operator, "instanceof": operator, - "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, - "this": kw("this"), "class": kw("class"), "super": kw("atom"), - "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, - "await": C - }; - - // Extend the 'normal' keywords with the TypeScript language extensions - if (isTS) { - var type = {type: "variable", style: "type"}; - var tsKeywords = { - // object-like things - "interface": kw("class"), - "implements": C, - "namespace": C, - "module": kw("module"), - "enum": kw("module"), - - // scope modifiers - "public": kw("modifier"), - "private": kw("modifier"), - "protected": kw("modifier"), - "abstract": kw("modifier"), - - // types - "string": type, "number": type, "boolean": type, "any": type - }; - - for (var attr in tsKeywords) { - jsKeywords[attr] = tsKeywords[attr]; - } - } - - return jsKeywords; - }(); - - var isOperatorChar = /[+\-*&%=<>!?|~^@]/; - var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; - - function readRegexp(stream) { - var escaped = false, next, inSet = false; - while ((next = stream.next()) != null) { - if (!escaped) { - if (next == "/" && !inSet) return; - if (next == "[") inSet = true; - else if (inSet && next == "]") inSet = false; - } - escaped = !escaped && next == "\\"; - } - } - - // Used as scratch variables to communicate multiple values without - // consing up tons of objects. - var type, content; - function ret(tp, style, cont) { - type = tp; content = cont; - return style; - } - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { - return ret("number", "number"); - } else if (ch == "." && stream.match("..")) { - return ret("spread", "meta"); - } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - return ret(ch); - } else if (ch == "=" && stream.eat(">")) { - return ret("=>", "operator"); - } else if (ch == "0" && stream.eat(/x/i)) { - stream.eatWhile(/[\da-f]/i); - return ret("number", "number"); - } else if (ch == "0" && stream.eat(/o/i)) { - stream.eatWhile(/[0-7]/i); - return ret("number", "number"); - } else if (ch == "0" && stream.eat(/b/i)) { - stream.eatWhile(/[01]/i); - return ret("number", "number"); - } else if (/\d/.test(ch)) { - stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); - return ret("number", "number"); - } else if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } else if (stream.eat("/")) { - stream.skipToEnd(); - return ret("comment", "comment"); - } else if (expressionAllowed(stream, state, 1)) { - readRegexp(stream); - stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/); - return ret("regexp", "string-2"); - } else { - stream.eatWhile(isOperatorChar); - return ret("operator", "operator", stream.current()); - } - } else if (ch == "`") { - state.tokenize = tokenQuasi; - return tokenQuasi(stream, state); - } else if (ch == "#") { - stream.skipToEnd(); - return ret("error", "error"); - } else if (isOperatorChar.test(ch)) { - if (ch != ">" || !state.lexical || state.lexical.type != ">") - stream.eatWhile(isOperatorChar); - return ret("operator", "operator", stream.current()); - } else if (wordRE.test(ch)) { - stream.eatWhile(wordRE); - var word = stream.current() - if (state.lastType != ".") { - if (keywords.propertyIsEnumerable(word)) { - var kw = keywords[word] - return ret(kw.type, kw.style, word) - } - if (word == "async" && stream.match(/^\s*[\(\w]/, false)) - return ret("async", "keyword", word) - } - return ret("variable", "variable", word) - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, next; - if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ - state.tokenize = tokenBase; - return ret("jsonld-keyword", "meta"); - } - while ((next = stream.next()) != null) { - if (next == quote && !escaped) break; - escaped = !escaped && next == "\\"; - } - if (!escaped) state.tokenize = tokenBase; - return ret("string", "string"); - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return ret("comment", "comment"); - } - - function tokenQuasi(stream, state) { - var escaped = false, next; - while ((next = stream.next()) != null) { - if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { - state.tokenize = tokenBase; - break; - } - escaped = !escaped && next == "\\"; - } - return ret("quasi", "string-2", stream.current()); - } - - var brackets = "([{}])"; - // This is a crude lookahead trick to try and notice that we're - // parsing the argument patterns for a fat-arrow function before we - // actually hit the arrow token. It only works if the arrow is on - // the same line as the arguments and there's no strange noise - // (comments) in between. Fallback is to only notice when we hit the - // arrow, and not declare the arguments as locals for the arrow - // body. - function findFatArrow(stream, state) { - if (state.fatArrowAt) state.fatArrowAt = null; - var arrow = stream.string.indexOf("=>", stream.start); - if (arrow < 0) return; - - if (isTS) { // Try to skip TypeScript return type declarations after the arguments - var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) - if (m) arrow = m.index - } - - var depth = 0, sawSomething = false; - for (var pos = arrow - 1; pos >= 0; --pos) { - var ch = stream.string.charAt(pos); - var bracket = brackets.indexOf(ch); - if (bracket >= 0 && bracket < 3) { - if (!depth) { ++pos; break; } - if (--depth == 0) { if (ch == "(") sawSomething = true; break; } - } else if (bracket >= 3 && bracket < 6) { - ++depth; - } else if (wordRE.test(ch)) { - sawSomething = true; - } else if (/["'\/]/.test(ch)) { - return; - } else if (sawSomething && !depth) { - ++pos; - break; - } - } - if (sawSomething && !depth) state.fatArrowAt = pos; - } - - // Parser - - var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; - - function JSLexical(indented, column, type, align, prev, info) { - this.indented = indented; - this.column = column; - this.type = type; - this.prev = prev; - this.info = info; - if (align != null) this.align = align; - } - - function inScope(state, varname) { - for (var v = state.localVars; v; v = v.next) - if (v.name == varname) return true; - for (var cx = state.context; cx; cx = cx.prev) { - for (var v = cx.vars; v; v = v.next) - if (v.name == varname) return true; - } - } - - function parseJS(state, style, type, content, stream) { - var cc = state.cc; - // Communicate our context to the combinators. - // (Less wasteful than consing up a hundred closures on every call.) - cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; - - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = true; - - while(true) { - var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; - if (combinator(type, content)) { - while(cc.length && cc[cc.length - 1].lex) - cc.pop()(); - if (cx.marked) return cx.marked; - if (type == "variable" && inScope(state, content)) return "variable-2"; - return style; - } - } - } - - // Combinator utils - - var cx = {state: null, column: null, marked: null, cc: null}; - function pass() { - for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); - } - function cont() { - pass.apply(null, arguments); - return true; - } - function register(varname) { - function inList(list) { - for (var v = list; v; v = v.next) - if (v.name == varname) return true; - return false; - } - var state = cx.state; - cx.marked = "def"; - if (state.context) { - if (inList(state.localVars)) return; - state.localVars = {name: varname, next: state.localVars}; - } else { - if (inList(state.globalVars)) return; - if (parserConfig.globalVars) - state.globalVars = {name: varname, next: state.globalVars}; - } - } - - // Combinators - - var defaultVars = {name: "this", next: {name: "arguments"}}; - function pushcontext() { - cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; - cx.state.localVars = defaultVars; - } - function popcontext() { - cx.state.localVars = cx.state.context.vars; - cx.state.context = cx.state.context.prev; - } - function pushlex(type, info) { - var result = function() { - var state = cx.state, indent = state.indented; - if (state.lexical.type == "stat") indent = state.lexical.indented; - else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) - indent = outer.indented; - state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); - }; - result.lex = true; - return result; - } - function poplex() { - var state = cx.state; - if (state.lexical.prev) { - if (state.lexical.type == ")") - state.indented = state.lexical.indented; - state.lexical = state.lexical.prev; - } - } - poplex.lex = true; - - function expect(wanted) { - function exp(type) { - if (type == wanted) return cont(); - else if (wanted == ";") return pass(); - else return cont(exp); - }; - return exp; - } - - function statement(type, value) { - if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); - if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); - if (type == "keyword b") return cont(pushlex("form"), statement, poplex); - if (type == "{") return cont(pushlex("}"), block, poplex); - if (type == ";") return cont(); - if (type == "if") { - if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) - cx.state.cc.pop()(); - return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); - } - if (type == "function") return cont(functiondef); - if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); - if (type == "variable") { - if (isTS && value == "type") { - cx.marked = "keyword" - return cont(typeexpr, expect("operator"), typeexpr, expect(";")); - } else { - return cont(pushlex("stat"), maybelabel); - } - } - if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), - block, poplex, poplex); - if (type == "case") return cont(expression, expect(":")); - if (type == "default") return cont(expect(":")); - if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), - statement, poplex, popcontext); - if (type == "class") return cont(pushlex("form"), className, poplex); - if (type == "export") return cont(pushlex("stat"), afterExport, poplex); - if (type == "import") return cont(pushlex("stat"), afterImport, poplex); - if (type == "module") return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) - if (type == "async") return cont(statement) - if (value == "@") return cont(expression, statement) - return pass(pushlex("stat"), expression, expect(";"), poplex); - } - function expression(type) { - return expressionInner(type, false); - } - function expressionNoComma(type) { - return expressionInner(type, true); - } - function parenExpr(type) { - if (type != "(") return pass() - return cont(pushlex(")"), expression, expect(")"), poplex) - } - function expressionInner(type, noComma) { - if (cx.state.fatArrowAt == cx.stream.start) { - var body = noComma ? arrowBodyNoComma : arrowBody; - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext); - else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); - } - - var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; - if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); - if (type == "function") return cont(functiondef, maybeop); - if (type == "class") return cont(pushlex("form"), classExpression, poplex); - if (type == "keyword c" || type == "async") return cont(noComma ? maybeexpressionNoComma : maybeexpression); - if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); - if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); - if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); - if (type == "{") return contCommasep(objprop, "}", null, maybeop); - if (type == "quasi") return pass(quasi, maybeop); - if (type == "new") return cont(maybeTarget(noComma)); - return cont(); - } - function maybeexpression(type) { - if (type.match(/[;\}\)\],]/)) return pass(); - return pass(expression); - } - function maybeexpressionNoComma(type) { - if (type.match(/[;\}\)\],]/)) return pass(); - return pass(expressionNoComma); - } - - function maybeoperatorComma(type, value) { - if (type == ",") return cont(expression); - return maybeoperatorNoComma(type, value, false); - } - function maybeoperatorNoComma(type, value, noComma) { - var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; - var expr = noComma == false ? expression : expressionNoComma; - if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); - if (type == "operator") { - if (/\+\+|--/.test(value)) return cont(me); - if (value == "?") return cont(expression, expect(":"), expr); - return cont(expr); - } - if (type == "quasi") { return pass(quasi, me); } - if (type == ";") return; - if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); - if (type == ".") return cont(property, me); - if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); - if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } - } - function quasi(type, value) { - if (type != "quasi") return pass(); - if (value.slice(value.length - 2) != "${") return cont(quasi); - return cont(expression, continueQuasi); - } - function continueQuasi(type) { - if (type == "}") { - cx.marked = "string-2"; - cx.state.tokenize = tokenQuasi; - return cont(quasi); - } - } - function arrowBody(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expression); - } - function arrowBodyNoComma(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expressionNoComma); - } - function maybeTarget(noComma) { - return function(type) { - if (type == ".") return cont(noComma ? targetNoComma : target); - else return pass(noComma ? expressionNoComma : expression); - }; - } - function target(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } - } - function targetNoComma(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } - } - function maybelabel(type) { - if (type == ":") return cont(poplex, statement); - return pass(maybeoperatorComma, expect(";"), poplex); - } - function property(type) { - if (type == "variable") {cx.marked = "property"; return cont();} - } - function objprop(type, value) { - if (type == "async") { - cx.marked = "property"; - return cont(objprop); - } else if (type == "variable" || cx.style == "keyword") { - cx.marked = "property"; - if (value == "get" || value == "set") return cont(getterSetter); - return cont(afterprop); - } else if (type == "number" || type == "string") { - cx.marked = jsonldMode ? "property" : (cx.style + " property"); - return cont(afterprop); - } else if (type == "jsonld-keyword") { - return cont(afterprop); - } else if (type == "modifier") { - return cont(objprop) - } else if (type == "[") { - return cont(expression, expect("]"), afterprop); - } else if (type == "spread") { - return cont(expression, afterprop); - } else if (type == ":") { - return pass(afterprop) - } - } - function getterSetter(type) { - if (type != "variable") return pass(afterprop); - cx.marked = "property"; - return cont(functiondef); - } - function afterprop(type) { - if (type == ":") return cont(expressionNoComma); - if (type == "(") return pass(functiondef); - } - function commasep(what, end, sep) { - function proceed(type, value) { - if (sep ? sep.indexOf(type) > -1 : type == ",") { - var lex = cx.state.lexical; - if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; - return cont(function(type, value) { - if (type == end || value == end) return pass() - return pass(what) - }, proceed); - } - if (type == end || value == end) return cont(); - return cont(expect(end)); - } - return function(type, value) { - if (type == end || value == end) return cont(); - return pass(what, proceed); - }; - } - function contCommasep(what, end, info) { - for (var i = 3; i < arguments.length; i++) - cx.cc.push(arguments[i]); - return cont(pushlex(end, info), commasep(what, end), poplex); - } - function block(type) { - if (type == "}") return cont(); - return pass(statement, block); - } - function maybetype(type, value) { - if (isTS) { - if (type == ":") return cont(typeexpr); - if (value == "?") return cont(maybetype); - } - } - function typeexpr(type) { - if (type == "variable") {cx.marked = "type"; return cont(afterType);} - if (type == "string" || type == "number" || type == "atom") return cont(afterType); - if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) - if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType) - } - function maybeReturnType(type) { - if (type == "=>") return cont(typeexpr) - } - function typeprop(type, value) { - if (type == "variable" || cx.style == "keyword") { - cx.marked = "property" - return cont(typeprop) - } else if (value == "?") { - return cont(typeprop) - } else if (type == ":") { - return cont(typeexpr) - } else if (type == "[") { - return cont(expression, maybetype, expect("]"), typeprop) - } - } - function typearg(type) { - if (type == "variable") return cont(typearg) - else if (type == ":") return cont(typeexpr) - } - function afterType(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) - if (value == "|" || type == ".") return cont(typeexpr) - if (type == "[") return cont(expect("]"), afterType) - if (value == "extends") return cont(typeexpr) - } - function vardef() { - return pass(pattern, maybetype, maybeAssign, vardefCont); - } - function pattern(type, value) { - if (type == "modifier") return cont(pattern) - if (type == "variable") { register(value); return cont(); } - if (type == "spread") return cont(pattern); - if (type == "[") return contCommasep(pattern, "]"); - if (type == "{") return contCommasep(proppattern, "}"); - } - function proppattern(type, value) { - if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { - register(value); - return cont(maybeAssign); - } - if (type == "variable") cx.marked = "property"; - if (type == "spread") return cont(pattern); - if (type == "}") return pass(); - return cont(expect(":"), pattern, maybeAssign); - } - function maybeAssign(_type, value) { - if (value == "=") return cont(expressionNoComma); - } - function vardefCont(type) { - if (type == ",") return cont(vardef); - } - function maybeelse(type, value) { - if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); - } - function forspec(type) { - if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); - } - function forspec1(type) { - if (type == "var") return cont(vardef, expect(";"), forspec2); - if (type == ";") return cont(forspec2); - if (type == "variable") return cont(formaybeinof); - return pass(expression, expect(";"), forspec2); - } - function formaybeinof(_type, value) { - if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } - return cont(maybeoperatorComma, forspec2); - } - function forspec2(type, value) { - if (type == ";") return cont(forspec3); - if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } - return pass(expression, expect(";"), forspec3); - } - function forspec3(type) { - if (type != ")") cont(expression); - } - function functiondef(type, value) { - if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} - if (type == "variable") {register(value); return cont(functiondef);} - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext); - if (isTS && value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, functiondef) - } - function funarg(type) { - if (type == "spread") return cont(funarg); - return pass(pattern, maybetype, maybeAssign); - } - function classExpression(type, value) { - // Class expressions may have an optional name. - if (type == "variable") return className(type, value); - return classNameAfter(type, value); - } - function className(type, value) { - if (type == "variable") {register(value); return cont(classNameAfter);} - } - function classNameAfter(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, classNameAfter) - if (value == "extends" || value == "implements" || (isTS && type == ",")) - return cont(isTS ? typeexpr : expression, classNameAfter); - if (type == "{") return cont(pushlex("}"), classBody, poplex); - } - function classBody(type, value) { - if (type == "variable" || cx.style == "keyword") { - if ((value == "async" || value == "static" || value == "get" || value == "set" || - (isTS && (value == "public" || value == "private" || value == "protected" || value == "readonly" || value == "abstract"))) && - cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false)) { - cx.marked = "keyword"; - return cont(classBody); - } - cx.marked = "property"; - return cont(isTS ? classfield : functiondef, classBody); - } - if (type == "[") - return cont(expression, expect("]"), isTS ? classfield : functiondef, classBody) - if (value == "*") { - cx.marked = "keyword"; - return cont(classBody); - } - if (type == ";") return cont(classBody); - if (type == "}") return cont(); - if (value == "@") return cont(expression, classBody) - } - function classfield(type, value) { - if (value == "?") return cont(classfield) - if (type == ":") return cont(typeexpr, maybeAssign) - if (value == "=") return cont(expressionNoComma) - return pass(functiondef) - } - function afterExport(type, value) { - if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } - if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } - if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); - return pass(statement); - } - function exportField(type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } - if (type == "variable") return pass(expressionNoComma, exportField); - } - function afterImport(type) { - if (type == "string") return cont(); - return pass(importSpec, maybeMoreImports, maybeFrom); - } - function importSpec(type, value) { - if (type == "{") return contCommasep(importSpec, "}"); - if (type == "variable") register(value); - if (value == "*") cx.marked = "keyword"; - return cont(maybeAs); - } - function maybeMoreImports(type) { - if (type == ",") return cont(importSpec, maybeMoreImports) - } - function maybeAs(_type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } - } - function maybeFrom(_type, value) { - if (value == "from") { cx.marked = "keyword"; return cont(expression); } - } - function arrayLiteral(type) { - if (type == "]") return cont(); - return pass(commasep(expressionNoComma, "]")); - } - - function isContinuedStatement(state, textAfter) { - return state.lastType == "operator" || state.lastType == "," || - isOperatorChar.test(textAfter.charAt(0)) || - /[,.]/.test(textAfter.charAt(0)); - } - - // Interface - - return { - startState: function(basecolumn) { - var state = { - tokenize: tokenBase, - lastType: "sof", - cc: [], - lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), - localVars: parserConfig.localVars, - context: parserConfig.localVars && {vars: parserConfig.localVars}, - indented: basecolumn || 0 - }; - if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") - state.globalVars = parserConfig.globalVars; - return state; - }, - - token: function(stream, state) { - if (stream.sol()) { - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = false; - state.indented = stream.indentation(); - findFatArrow(stream, state); - } - if (state.tokenize != tokenComment && stream.eatSpace()) return null; - var style = state.tokenize(stream, state); - if (type == "comment") return style; - state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; - return parseJS(state, style, type, content, stream); - }, - - indent: function(state, textAfter) { - if (state.tokenize == tokenComment) return CodeMirror.Pass; - if (state.tokenize != tokenBase) return 0; - var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top - // Kludge to prevent 'maybelse' from blocking lexical scope pops - if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { - var c = state.cc[i]; - if (c == poplex) lexical = lexical.prev; - else if (c != maybeelse) break; - } - while ((lexical.type == "stat" || lexical.type == "form") && - (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && - (top == maybeoperatorComma || top == maybeoperatorNoComma) && - !/^[,\.=+\-*:?[\(]/.test(textAfter)))) - lexical = lexical.prev; - if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") - lexical = lexical.prev; - var type = lexical.type, closing = firstChar == type; - - if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); - else if (type == "form" && firstChar == "{") return lexical.indented; - else if (type == "form") return lexical.indented + indentUnit; - else if (type == "stat") - return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); - else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) - return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); - else if (lexical.align) return lexical.column + (closing ? 0 : 1); - else return lexical.indented + (closing ? 0 : indentUnit); - }, - - electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, - blockCommentStart: jsonMode ? null : "/*", - blockCommentEnd: jsonMode ? null : "*/", - lineComment: jsonMode ? null : "//", - fold: "brace", - closeBrackets: "()[]{}''\"\"``", - - helperType: jsonMode ? "json" : "javascript", - jsonldMode: jsonldMode, - jsonMode: jsonMode, - - expressionAllowed: expressionAllowed, - skipExpression: function(state) { - var top = state.cc[state.cc.length - 1] - if (top == expression || top == expressionNoComma) state.cc.pop() - } - }; -}); - -CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); - -CodeMirror.defineMIME("text/javascript", "javascript"); -CodeMirror.defineMIME("text/ecmascript", "javascript"); -CodeMirror.defineMIME("application/javascript", "javascript"); -CodeMirror.defineMIME("application/x-javascript", "javascript"); -CodeMirror.defineMIME("application/ecmascript", "javascript"); -CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); -CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); -CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); - -}); diff --git a/js/DevHelper/Lib/CodeMirror/mode/javascript/json-ld.html b/js/DevHelper/Lib/CodeMirror/mode/javascript/json-ld.html deleted file mode 100644 index 3a37f0b..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/javascript/json-ld.html +++ /dev/null @@ -1,72 +0,0 @@ - - -CodeMirror: JSON-LD mode - - - - - - - - - - - - -
-

JSON-LD mode

- - -
- - - -

This is a specialization of the JavaScript mode.

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/javascript/test.js b/js/DevHelper/Lib/CodeMirror/mode/javascript/test.js deleted file mode 100644 index 328cbc2..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/javascript/test.js +++ /dev/null @@ -1,379 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - var mode = CodeMirror.getMode({indentUnit: 2}, "javascript"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } - - MT("locals", - "[keyword function] [def foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }"); - - MT("comma-and-binop", - "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }"); - - MT("destructuring", - "([keyword function]([def a], [[[def b], [def c] ]]) {", - " [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);", - " [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];", - "})();"); - - MT("destructure_trailing_comma", - "[keyword let] {[def a], [def b],} [operator =] [variable foo];", - "[keyword let] [def c];"); // Parser still in good state? - - MT("class_body", - "[keyword class] [def Foo] {", - " [property constructor]() {}", - " [property sayName]() {", - " [keyword return] [string-2 `foo${][variable foo][string-2 }oo`];", - " }", - "}"); - - MT("class", - "[keyword class] [def Point] [keyword extends] [variable SuperThing] {", - " [keyword get] [property prop]() { [keyword return] [number 24]; }", - " [property constructor]([def x], [def y]) {", - " [keyword super]([string 'something']);", - " [keyword this].[property x] [operator =] [variable-2 x];", - " }", - "}"); - - MT("anonymous_class_expression", - "[keyword const] [def Adder] [operator =] [keyword class] [keyword extends] [variable Arithmetic] {", - " [property add]([def a], [def b]) {}", - "};"); - - MT("named_class_expression", - "[keyword const] [def Subber] [operator =] [keyword class] [def Subtract] {", - " [property sub]([def a], [def b]) {}", - "};"); - - MT("class_async_method", - "[keyword class] [def Foo] {", - " [property sayName1]() {}", - " [keyword async] [property sayName2]() {}", - "}"); - - MT("import", - "[keyword function] [def foo]() {", - " [keyword import] [def $] [keyword from] [string 'jquery'];", - " [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];", - "}"); - - MT("import_trailing_comma", - "[keyword import] {[def foo], [def bar],} [keyword from] [string 'baz']") - - MT("const", - "[keyword function] [def f]() {", - " [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];", - "}"); - - MT("for/of", - "[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}"); - - MT("generator", - "[keyword function*] [def repeat]([def n]) {", - " [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])", - " [keyword yield] [variable-2 i];", - "}"); - - MT("quotedStringAddition", - "[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];"); - - MT("quotedFatArrow", - "[keyword let] [def f] [operator =] [variable a] [operator +] [string '=>'] [operator +] [variable c];"); - - MT("fatArrow", - "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);", - "[variable a];", // No longer in scope - "[keyword let] [def f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];", - "[variable c];"); - - MT("spread", - "[keyword function] [def f]([def a], [meta ...][def b]) {", - " [variable something]([variable-2 a], [meta ...][variable-2 b]);", - "}"); - - MT("quasi", - "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]"); - - MT("quasi_no_function", - "[variable x] [operator =] [string-2 `fofdlakj${][variable x] [operator +] [string-2 `foo`] [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]"); - - MT("indent_statement", - "[keyword var] [def x] [operator =] [number 10]", - "[variable x] [operator +=] [variable y] [operator +]", - " [atom Infinity]", - "[keyword debugger];"); - - MT("indent_if", - "[keyword if] ([number 1])", - " [keyword break];", - "[keyword else] [keyword if] ([number 2])", - " [keyword continue];", - "[keyword else]", - " [number 10];", - "[keyword if] ([number 1]) {", - " [keyword break];", - "} [keyword else] [keyword if] ([number 2]) {", - " [keyword continue];", - "} [keyword else] {", - " [number 10];", - "}"); - - MT("indent_for", - "[keyword for] ([keyword var] [def i] [operator =] [number 0];", - " [variable i] [operator <] [number 100];", - " [variable i][operator ++])", - " [variable doSomething]([variable i]);", - "[keyword debugger];"); - - MT("indent_c_style", - "[keyword function] [def foo]()", - "{", - " [keyword debugger];", - "}"); - - MT("indent_else", - "[keyword for] (;;)", - " [keyword if] ([variable foo])", - " [keyword if] ([variable bar])", - " [number 1];", - " [keyword else]", - " [number 2];", - " [keyword else]", - " [number 3];"); - - MT("indent_funarg", - "[variable foo]([number 10000],", - " [keyword function]([def a]) {", - " [keyword debugger];", - "};"); - - MT("indent_below_if", - "[keyword for] (;;)", - " [keyword if] ([variable foo])", - " [number 1];", - "[number 2];"); - - MT("indent_semicolonless_if", - "[keyword function] [def foo]() {", - " [keyword if] ([variable x])", - " [variable foo]()", - "}") - - MT("indent_semicolonless_if_with_statement", - "[keyword function] [def foo]() {", - " [keyword if] ([variable x])", - " [variable foo]()", - " [variable bar]()", - "}") - - MT("multilinestring", - "[keyword var] [def x] [operator =] [string 'foo\\]", - "[string bar'];"); - - MT("scary_regexp", - "[string-2 /foo[[/]]bar/];"); - - MT("indent_strange_array", - "[keyword var] [def x] [operator =] [[", - " [number 1],,", - " [number 2],", - "]];", - "[number 10];"); - - MT("param_default", - "[keyword function] [def foo]([def x] [operator =] [string-2 `foo${][number 10][string-2 }bar`]) {", - " [keyword return] [variable-2 x];", - "}"); - - MT("new_target", - "[keyword function] [def F]([def target]) {", - " [keyword if] ([variable-2 target] [operator &&] [keyword new].[keyword target].[property name]) {", - " [keyword return] [keyword new]", - " .[keyword target];", - " }", - "}"); - - MT("async", - "[keyword async] [keyword function] [def foo]([def args]) { [keyword return] [atom true]; }"); - - MT("async_assignment", - "[keyword const] [def foo] [operator =] [keyword async] [keyword function] ([def args]) { [keyword return] [atom true]; };"); - - MT("async_object", - "[keyword let] [def obj] [operator =] { [property async]: [atom false] };"); - - // async be highlighet as keyword and foo as def, but it requires potentially expensive look-ahead. See #4173 - MT("async_object_function", - "[keyword let] [def obj] [operator =] { [property async] [property foo]([def args]) { [keyword return] [atom true]; } };"); - - MT("async_object_properties", - "[keyword let] [def obj] [operator =] {", - " [property prop1]: [keyword async] [keyword function] ([def args]) { [keyword return] [atom true]; },", - " [property prop2]: [keyword async] [keyword function] ([def args]) { [keyword return] [atom true]; },", - " [property prop3]: [keyword async] [keyword function] [def prop3]([def args]) { [keyword return] [atom true]; },", - "};"); - - MT("async_arrow", - "[keyword const] [def foo] [operator =] [keyword async] ([def args]) [operator =>] { [keyword return] [atom true]; };"); - - MT("async_jquery", - "[variable $].[property ajax]({", - " [property url]: [variable url],", - " [property async]: [atom true],", - " [property method]: [string 'GET']", - "});"); - - MT("async_variable", - "[keyword const] [def async] [operator =] {[property a]: [number 1]};", - "[keyword const] [def foo] [operator =] [string-2 `bar ${][variable async].[property a][string-2 }`];") - - MT("indent_switch", - "[keyword switch] ([variable x]) {", - " [keyword default]:", - " [keyword return] [number 2]", - "}") - - var ts_mode = CodeMirror.getMode({indentUnit: 2}, "application/typescript") - function TS(name) { - test.mode(name, ts_mode, Array.prototype.slice.call(arguments, 1)) - } - - TS("typescript_extend_type", - "[keyword class] [def Foo] [keyword extends] [type Some][operator <][type Type][operator >] {}") - - TS("typescript_arrow_type", - "[keyword let] [def x]: ([variable arg]: [type Type]) [operator =>] [type ReturnType]") - - TS("typescript_class", - "[keyword class] [def Foo] {", - " [keyword public] [keyword static] [property main]() {}", - " [keyword private] [property _foo]: [type string];", - "}") - - TS("typescript_literal_types", - "[keyword import] [keyword *] [keyword as] [def Sequelize] [keyword from] [string 'sequelize'];", - "[keyword interface] [def MyAttributes] {", - " [property truthy]: [string 'true'] [operator |] [number 1] [operator |] [atom true];", - " [property falsy]: [string 'false'] [operator |] [number 0] [operator |] [atom false];", - "}", - "[keyword interface] [def MyInstance] [keyword extends] [type Sequelize].[type Instance] [operator <] [type MyAttributes] [operator >] {", - " [property rawAttributes]: [type MyAttributes];", - " [property truthy]: [string 'true'] [operator |] [number 1] [operator |] [atom true];", - " [property falsy]: [string 'false'] [operator |] [number 0] [operator |] [atom false];", - "}") - - TS("typescript_extend_operators", - "[keyword export] [keyword interface] [def UserModel] [keyword extends]", - " [type Sequelize].[type Model] [operator <] [type UserInstance], [type UserAttributes] [operator >] {", - " [property findById]: (", - " [variable userId]: [type number]", - " ) [operator =>] [type Promise] [operator <] [type Array] [operator <] { [property id], [property name] } [operator >>];", - " [property updateById]: (", - " [variable userId]: [type number],", - " [variable isActive]: [type boolean]", - " ) [operator =>] [type Promise] [operator <] [type AccountHolderNotificationPreferenceInstance] [operator >];", - " }") - - TS("typescript_interface_with_const", - "[keyword const] [def hello]: {", - " [property prop1][operator ?]: [type string];", - " [property prop2][operator ?]: [type string];", - "} [operator =] {};") - - TS("typescript_double_extend", - "[keyword export] [keyword interface] [def UserAttributes] {", - " [property id][operator ?]: [type number];", - " [property createdAt][operator ?]: [type Date];", - "}", - "[keyword export] [keyword interface] [def UserInstance] [keyword extends] [type Sequelize].[type Instance][operator <][type UserAttributes][operator >], [type UserAttributes] {", - " [property id]: [type number];", - " [property createdAt]: [type Date];", - "}"); - - TS("typescript_index_signature", - "[keyword interface] [def A] {", - " [[ [variable prop]: [type string] ]]: [type any];", - " [property prop1]: [type any];", - "}"); - - TS("typescript_generic_class", - "[keyword class] [def Foo][operator <][type T][operator >] {", - " [property bar]() {}", - " [property foo](): [type Foo] {}", - "}") - - TS("typescript_type_when_keyword", - "[keyword export] [keyword type] [type AB] [operator =] [type A] [operator |] [type B];", - "[keyword type] [type Flags] [operator =] {", - " [property p1]: [type string];", - " [property p2]: [type boolean];", - "};") - - TS("typescript_type_when_not_keyword", - "[keyword class] [def HasType] {", - " [property type]: [type string];", - " [property constructor]([def type]: [type string]) {", - " [keyword this].[property type] [operator =] [variable-2 type];", - " }", - " [property setType]({ [def type] }: { [property type]: [type string]; }) {", - " [keyword this].[property type] [operator =] [variable-2 type];", - " }", - "}") - - TS("typescript_function_generics", - "[keyword function] [def a]() {}", - "[keyword function] [def b][operator <][type IA] [keyword extends] [type object], [type IB] [keyword extends] [type object][operator >]() {}", - "[keyword function] [def c]() {}") - - TS("typescript_complex_return_type", - "[keyword function] [def A]() {", - " [keyword return] [keyword this].[property property];", - "}", - "[keyword function] [def B](): [type Promise][operator <]{ [[ [variable key]: [type string] ]]: [type any] } [operator |] [atom null][operator >] {", - " [keyword return] [keyword this].[property property];", - "}") - - TS("typescript_complex_type_casting", - "[keyword const] [def giftpay] [operator =] [variable config].[property get]([string 'giftpay']) [keyword as] { [[ [variable platformUuid]: [type string] ]]: { [property version]: [type number]; [property apiCode]: [type string]; } };") - - var jsonld_mode = CodeMirror.getMode( - {indentUnit: 2}, - {name: "javascript", jsonld: true} - ); - function LD(name) { - test.mode(name, jsonld_mode, Array.prototype.slice.call(arguments, 1)); - } - - LD("json_ld_keywords", - '{', - ' [meta "@context"]: {', - ' [meta "@base"]: [string "http://example.com"],', - ' [meta "@vocab"]: [string "http://xmlns.com/foaf/0.1/"],', - ' [property "likesFlavor"]: {', - ' [meta "@container"]: [meta "@list"]', - ' [meta "@reverse"]: [string "@beFavoriteOf"]', - ' },', - ' [property "nick"]: { [meta "@container"]: [meta "@set"] },', - ' [property "nick"]: { [meta "@container"]: [meta "@index"] }', - ' },', - ' [meta "@graph"]: [[ {', - ' [meta "@id"]: [string "http://dbpedia.org/resource/John_Lennon"],', - ' [property "name"]: [string "John Lennon"],', - ' [property "modified"]: {', - ' [meta "@value"]: [string "2010-05-29T14:17:39+02:00"],', - ' [meta "@type"]: [string "http://www.w3.org/2001/XMLSchema#dateTime"]', - ' }', - ' } ]]', - '}'); - - LD("json_ld_fake", - '{', - ' [property "@fake"]: [string "@fake"],', - ' [property "@contextual"]: [string "@identifier"],', - ' [property "user@domain.com"]: [string "@graphical"],', - ' [property "@ID"]: [string "@@ID"]', - '}'); -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/javascript/typescript.html b/js/DevHelper/Lib/CodeMirror/mode/javascript/typescript.html deleted file mode 100644 index 1f26d7f..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/javascript/typescript.html +++ /dev/null @@ -1,61 +0,0 @@ - - -CodeMirror: TypeScript mode - - - - - - - - - -
-

TypeScript mode

- - -
- - - -

This is a specialization of the JavaScript mode.

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/xml/index.html b/js/DevHelper/Lib/CodeMirror/mode/xml/index.html deleted file mode 100644 index c56b8b6..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/xml/index.html +++ /dev/null @@ -1,61 +0,0 @@ - - -CodeMirror: XML mode - - - - - - - - - -
-

XML mode

-
- -

The XML mode supports these configuration parameters:

-
-
htmlMode (boolean)
-
This switches the mode to parse HTML instead of XML. This - means attributes do not have to be quoted, and some elements - (such as br) do not require a closing tag.
-
matchClosing (boolean)
-
Controls whether the mode checks that close tags match the - corresponding opening tag, and highlights mismatches as errors. - Defaults to true.
-
alignCDATA (boolean)
-
Setting this to true will force the opening tag of CDATA - blocks to not be indented.
-
- -

MIME types defined: application/xml, text/html.

-
diff --git a/js/DevHelper/Lib/CodeMirror/mode/xml/test.js b/js/DevHelper/Lib/CodeMirror/mode/xml/test.js deleted file mode 100644 index f48156b..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/xml/test.js +++ /dev/null @@ -1,51 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - var mode = CodeMirror.getMode({indentUnit: 2}, "xml"), mname = "xml"; - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), mname); } - - MT("matching", - "[tag&bracket <][tag top][tag&bracket >]", - " text", - " [tag&bracket <][tag inner][tag&bracket />]", - "[tag&bracket ]"); - - MT("nonmatching", - "[tag&bracket <][tag top][tag&bracket >]", - " [tag&bracket <][tag inner][tag&bracket />]", - " [tag&bracket ]"); - - MT("doctype", - "[meta ]", - "[tag&bracket <][tag top][tag&bracket />]"); - - MT("cdata", - "[tag&bracket <][tag top][tag&bracket >]", - " [atom ]", - "[tag&bracket ]"); - - // HTML tests - mode = CodeMirror.getMode({indentUnit: 2}, "text/html"); - - MT("selfclose", - "[tag&bracket <][tag html][tag&bracket >]", - " [tag&bracket <][tag link] [attribute rel]=[string stylesheet] [attribute href]=[string \"/foobar\"][tag&bracket >]", - "[tag&bracket ]"); - - MT("list", - "[tag&bracket <][tag ol][tag&bracket >]", - " [tag&bracket <][tag li][tag&bracket >]one", - " [tag&bracket <][tag li][tag&bracket >]two", - "[tag&bracket ]"); - - MT("valueless", - "[tag&bracket <][tag input] [attribute type]=[string checkbox] [attribute checked][tag&bracket />]"); - - MT("pThenArticle", - "[tag&bracket <][tag p][tag&bracket >]", - " foo", - "[tag&bracket <][tag article][tag&bracket >]bar"); - -})(); diff --git a/js/DevHelper/Lib/CodeMirror/mode/xml/xml.js b/js/DevHelper/Lib/CodeMirror/mode/xml/xml.js deleted file mode 100644 index f987a3a..0000000 --- a/js/DevHelper/Lib/CodeMirror/mode/xml/xml.js +++ /dev/null @@ -1,394 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -var htmlConfig = { - autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, - 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, - 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, - 'track': true, 'wbr': true, 'menuitem': true}, - implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, - 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, - 'th': true, 'tr': true}, - contextGrabbers: { - 'dd': {'dd': true, 'dt': true}, - 'dt': {'dd': true, 'dt': true}, - 'li': {'li': true}, - 'option': {'option': true, 'optgroup': true}, - 'optgroup': {'optgroup': true}, - 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, - 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, - 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, - 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, - 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, - 'rp': {'rp': true, 'rt': true}, - 'rt': {'rp': true, 'rt': true}, - 'tbody': {'tbody': true, 'tfoot': true}, - 'td': {'td': true, 'th': true}, - 'tfoot': {'tbody': true}, - 'th': {'td': true, 'th': true}, - 'thead': {'tbody': true, 'tfoot': true}, - 'tr': {'tr': true} - }, - doNotIndent: {"pre": true}, - allowUnquoted: true, - allowMissing: true, - caseFold: true -} - -var xmlConfig = { - autoSelfClosers: {}, - implicitlyClosed: {}, - contextGrabbers: {}, - doNotIndent: {}, - allowUnquoted: false, - allowMissing: false, - caseFold: false -} - -CodeMirror.defineMode("xml", function(editorConf, config_) { - var indentUnit = editorConf.indentUnit - var config = {} - var defaults = config_.htmlMode ? htmlConfig : xmlConfig - for (var prop in defaults) config[prop] = defaults[prop] - for (var prop in config_) config[prop] = config_[prop] - - // Return variables for tokenizers - var type, setStyle; - - function inText(stream, state) { - function chain(parser) { - state.tokenize = parser; - return parser(stream, state); - } - - var ch = stream.next(); - if (ch == "<") { - if (stream.eat("!")) { - if (stream.eat("[")) { - if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); - else return null; - } else if (stream.match("--")) { - return chain(inBlock("comment", "-->")); - } else if (stream.match("DOCTYPE", true, true)) { - stream.eatWhile(/[\w\._\-]/); - return chain(doctype(1)); - } else { - return null; - } - } else if (stream.eat("?")) { - stream.eatWhile(/[\w\._\-]/); - state.tokenize = inBlock("meta", "?>"); - return "meta"; - } else { - type = stream.eat("/") ? "closeTag" : "openTag"; - state.tokenize = inTag; - return "tag bracket"; - } - } else if (ch == "&") { - var ok; - if (stream.eat("#")) { - if (stream.eat("x")) { - ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); - } else { - ok = stream.eatWhile(/[\d]/) && stream.eat(";"); - } - } else { - ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); - } - return ok ? "atom" : "error"; - } else { - stream.eatWhile(/[^&<]/); - return null; - } - } - inText.isInText = true; - - function inTag(stream, state) { - var ch = stream.next(); - if (ch == ">" || (ch == "/" && stream.eat(">"))) { - state.tokenize = inText; - type = ch == ">" ? "endTag" : "selfcloseTag"; - return "tag bracket"; - } else if (ch == "=") { - type = "equals"; - return null; - } else if (ch == "<") { - state.tokenize = inText; - state.state = baseState; - state.tagName = state.tagStart = null; - var next = state.tokenize(stream, state); - return next ? next + " tag error" : "tag error"; - } else if (/[\'\"]/.test(ch)) { - state.tokenize = inAttribute(ch); - state.stringStartCol = stream.column(); - return state.tokenize(stream, state); - } else { - stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); - return "word"; - } - } - - function inAttribute(quote) { - var closure = function(stream, state) { - while (!stream.eol()) { - if (stream.next() == quote) { - state.tokenize = inTag; - break; - } - } - return "string"; - }; - closure.isInAttribute = true; - return closure; - } - - function inBlock(style, terminator) { - return function(stream, state) { - while (!stream.eol()) { - if (stream.match(terminator)) { - state.tokenize = inText; - break; - } - stream.next(); - } - return style; - }; - } - function doctype(depth) { - return function(stream, state) { - var ch; - while ((ch = stream.next()) != null) { - if (ch == "<") { - state.tokenize = doctype(depth + 1); - return state.tokenize(stream, state); - } else if (ch == ">") { - if (depth == 1) { - state.tokenize = inText; - break; - } else { - state.tokenize = doctype(depth - 1); - return state.tokenize(stream, state); - } - } - } - return "meta"; - }; - } - - function Context(state, tagName, startOfLine) { - this.prev = state.context; - this.tagName = tagName; - this.indent = state.indented; - this.startOfLine = startOfLine; - if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) - this.noIndent = true; - } - function popContext(state) { - if (state.context) state.context = state.context.prev; - } - function maybePopContext(state, nextTagName) { - var parentTagName; - while (true) { - if (!state.context) { - return; - } - parentTagName = state.context.tagName; - if (!config.contextGrabbers.hasOwnProperty(parentTagName) || - !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { - return; - } - popContext(state); - } - } - - function baseState(type, stream, state) { - if (type == "openTag") { - state.tagStart = stream.column(); - return tagNameState; - } else if (type == "closeTag") { - return closeTagNameState; - } else { - return baseState; - } - } - function tagNameState(type, stream, state) { - if (type == "word") { - state.tagName = stream.current(); - setStyle = "tag"; - return attrState; - } else { - setStyle = "error"; - return tagNameState; - } - } - function closeTagNameState(type, stream, state) { - if (type == "word") { - var tagName = stream.current(); - if (state.context && state.context.tagName != tagName && - config.implicitlyClosed.hasOwnProperty(state.context.tagName)) - popContext(state); - if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) { - setStyle = "tag"; - return closeState; - } else { - setStyle = "tag error"; - return closeStateErr; - } - } else { - setStyle = "error"; - return closeStateErr; - } - } - - function closeState(type, _stream, state) { - if (type != "endTag") { - setStyle = "error"; - return closeState; - } - popContext(state); - return baseState; - } - function closeStateErr(type, stream, state) { - setStyle = "error"; - return closeState(type, stream, state); - } - - function attrState(type, _stream, state) { - if (type == "word") { - setStyle = "attribute"; - return attrEqState; - } else if (type == "endTag" || type == "selfcloseTag") { - var tagName = state.tagName, tagStart = state.tagStart; - state.tagName = state.tagStart = null; - if (type == "selfcloseTag" || - config.autoSelfClosers.hasOwnProperty(tagName)) { - maybePopContext(state, tagName); - } else { - maybePopContext(state, tagName); - state.context = new Context(state, tagName, tagStart == state.indented); - } - return baseState; - } - setStyle = "error"; - return attrState; - } - function attrEqState(type, stream, state) { - if (type == "equals") return attrValueState; - if (!config.allowMissing) setStyle = "error"; - return attrState(type, stream, state); - } - function attrValueState(type, stream, state) { - if (type == "string") return attrContinuedState; - if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;} - setStyle = "error"; - return attrState(type, stream, state); - } - function attrContinuedState(type, stream, state) { - if (type == "string") return attrContinuedState; - return attrState(type, stream, state); - } - - return { - startState: function(baseIndent) { - var state = {tokenize: inText, - state: baseState, - indented: baseIndent || 0, - tagName: null, tagStart: null, - context: null} - if (baseIndent != null) state.baseIndent = baseIndent - return state - }, - - token: function(stream, state) { - if (!state.tagName && stream.sol()) - state.indented = stream.indentation(); - - if (stream.eatSpace()) return null; - type = null; - var style = state.tokenize(stream, state); - if ((style || type) && style != "comment") { - setStyle = null; - state.state = state.state(type || style, stream, state); - if (setStyle) - style = setStyle == "error" ? style + " error" : setStyle; - } - return style; - }, - - indent: function(state, textAfter, fullLine) { - var context = state.context; - // Indent multi-line strings (e.g. css). - if (state.tokenize.isInAttribute) { - if (state.tagStart == state.indented) - return state.stringStartCol + 1; - else - return state.indented + indentUnit; - } - if (context && context.noIndent) return CodeMirror.Pass; - if (state.tokenize != inTag && state.tokenize != inText) - return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; - // Indent the starts of attribute names. - if (state.tagName) { - if (config.multilineTagIndentPastTag !== false) - return state.tagStart + state.tagName.length + 2; - else - return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1); - } - if (config.alignCDATA && /$/, - blockCommentStart: "", - - configuration: config.htmlMode ? "html" : "xml", - helperType: config.htmlMode ? "html" : "xml", - - skipAttribute: function(state) { - if (state.state == attrValueState) - state.state = attrState - } - }; -}); - -CodeMirror.defineMIME("text/xml", "xml"); -CodeMirror.defineMIME("application/xml", "xml"); -if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) - CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); - -}); diff --git a/js/DevHelper/full/template_edit.js b/js/DevHelper/full/template_edit.js deleted file mode 100755 index 64f19a4..0000000 --- a/js/DevHelper/full/template_edit.js +++ /dev/null @@ -1,132 +0,0 @@ -/** @param {jQuery} $ jQuery Object */ -!function ($) { - - var defaultExtraKeys = {}; - defaultExtraKeys['Tab'] = 'indentMore'; - defaultExtraKeys['Shift-Tab'] = 'indentLess'; - - var defaultConfig = { - mode: 'htmlmixed', - lineNumbers: 1, - indentWithTabs: 1, - smartIndent: 1, - tabSize: 4, - indentUnit: 4, - extraKeys: defaultExtraKeys - }; - - if (XenForo.TemplateEditor) { - var targetPrototype = XenForo.TemplateEditor.prototype; - - targetPrototype.DevHelper_extraKeysSave = function () { - $('#saveReloadButton').trigger('click'); - }; - - targetPrototype.DevHelper_setupCM = function (editor) { - var $textarea = editor.$textarea; - var $textareaWrapper = this.getTextareaWrapper(); - - // default to use the mixed HTML mode - var cmMode = 'htmlmixed'; - if (editor.$title.val().indexOf('.css') !== -1) { - // we are editing a CSS template - // switch to CSS mode - cmMode = 'css'; - } - - var config = defaultConfig; - config['value'] = $textarea.val(); - config['mode'] = cmMode; - - config.extraKeys['Cmd-S'] = $.context(this, 'DevHelper_extraKeysSave'); - config.extraKeys['Ctrl-S'] = $.context(this, 'DevHelper_extraKeysSave'); - - var theCM = CodeMirror(function () { - }, config); - theCM.on('change', function (cm) { - $textarea.val(cm.getValue()); - }); - - var $wrapper = $(theCM.getWrapperElement()); - $wrapper.width($textareaWrapper.parent().width()); - - // append the CodeMirror editor's wrapper to the page - $textareaWrapper.append($wrapper); - theCM.refresh(); - - // hide the textarea - $textarea.xfHide(); - - // save it for later access - editor.theCM = theCM; - editor.$theCMWrapper = $wrapper; - }; - - var originalInitializePrimaryEditor = targetPrototype.initializePrimaryEditor; - targetPrototype.initializePrimaryEditor = function () { - originalInitializePrimaryEditor.call(this); - - var templateTitle = this.$titleOriginal.strval(); - var editor = this.editors[templateTitle]; - - this.DevHelper_setupCM(editor); - }; - - var originalCreateEditor = targetPrototype.createEditor; - targetPrototype.createEditor = function (templateTitle, $prevTab) { - var editor = originalCreateEditor.call(this, templateTitle, $prevTab); - - this.DevHelper_setupCM(editor); - - // immediately hide the editor - editor.$theCMWrapper.xfHide(); - - return editor; - }; - - targetPrototype.switchEditor = function (e) { - var $target = $(e.target).closest('a'), - editor; - - // switch the active tab - $target.closest('li') - .addClass('active') - .siblings().removeClass('active'); - - // hide all CodeMirror instances - $('.CodeMirror', this.getTextareaWrapper()).xfHide(); - - editor = this.editors[$target.attr('templateTitle')]; - - // display the only needed one - editor.$theCMWrapper.xfShow(); - - return false; - }; - } - - XenForo.DevHelper_CodeMirror_TextArea = function ($textarea) { - $textarea.each(function () { - var config = defaultConfig; - - //config['viewportMargin'] = 'Infinity'; - - var functionSave = function () { - $textarea.parents('form').find('input.button.primary').trigger('click'); - }; - config.extraKeys['Cmd-S'] = functionSave; - config.extraKeys['Ctrl-S'] = functionSave; - - var theCM = CodeMirror.fromTextArea(this, defaultConfig); - - theCM.on('change', function (cm) { - $textarea.val(cm.getValue()); - }); - $(theCM.getWrapperElement()).addClass('DevHelper_CodeMirror_TextArea'); - }); - }; - - // support template modification `find` textarea - XenForo.register('textarea#ctrl_find.code', 'XenForo.DevHelper_CodeMirror_TextArea'); - XenForo.register('textarea#ctrl_replace.code', 'XenForo.DevHelper_CodeMirror_TextArea'); -}(jQuery); \ No newline at end of file diff --git a/library/DevHelper/Autoloader.php b/library/DevHelper/Autoloader.php deleted file mode 100644 index fd5f4fb..0000000 --- a/library/DevHelper/Autoloader.php +++ /dev/null @@ -1,175 +0,0 @@ -_setup) { - return; - } - - $this->_rootDir = $rootDir; - spl_autoload_register(array($this, 'autoload')); - - $this->_setup = true; - } - - public function autoload($class) - { - if (class_exists($class, false) || interface_exists($class, false)) { - return true; - } - - if ($class == 'utf8_entity_decoder') { - return false; - } - - if (substr($class, 0, 5) == 'XFCP_') { - return false; - } - - $filename = $this->autoloaderClassToFile($class); - if (!$filename) { - return false; - } - - if (file_exists($filename)) { - /** @noinspection PhpIncludeInspection */ - require($filename); - if (class_exists($class, false) || interface_exists($class, false)) { - return true; - } - } - - return false; - } - - public static function throwIfNotSetup() - { - if (empty($_SERVER['SCRIPT_FILENAME'])) { - throw new XenForo_Exception('Cannot get value for $_SERVER[\'SCRIPT_FILENAME\']'); - } - - /** @var XenForo_Application $app */ - $app = XenForo_Application::getInstance(); - $root = $app->getRootDir(); - $candidates = array( - file_get_contents($root . '/index.php'), - file_get_contents($root . '/admin.php'), - ); - - if (empty($_SERVER['DEVHELPER_ROUTER_PHP']) - && in_array(file_get_contents($_SERVER['SCRIPT_FILENAME']), $candidates, true) - && !self::$_instance - ) { - throw new XenForo_Exception('DevHelper_Autoloader must be setup'); - } - - if (!class_exists('DevHelper_XenForo_Autoloader')) { - eval('class DevHelper_XenForo_Autoloader extends XenForo_Autoloader { - public static function setInstanceCarelessly($loader = null) - { - self::$_instance = $loader; - } - }'); - } - - /** @noinspection PhpUndefinedClassInspection */ - DevHelper_XenForo_Autoloader::setInstanceCarelessly(self::$_instance); - } - - final public static function getDevHelperInstance() - { - if (!self::$_instance) { - self::$_instance = new self(); - } - - return self::$_instance; - } - - public function autoloaderClassToFile($class) - { - static $classes = array( - 'XenForo_CodeEvent', - 'XenForo_Debug', - 'XenForo_Template_Abstract', - 'XenForo_ViewRenderer_Json', - ); - if (in_array($class, $classes, true)) { - $class = 'DevHelper_' . $class; - } - - $classFile = $this->_autoloaderClassToFile($class); - $classFile = $this->_locateClassFile($classFile); - - $strPos = 0; - if (substr($class, 0, 9) !== 'DevHelper') { - $strPos = strpos($class, 'ShippableHelper_'); - } - if ($strPos > 0) { - // a helper class is being called, check its version vs. ours - $classVersionId = 0; - if (file_exists($classFile)) { - $classContents = file_get_contents($classFile); - - $classVersionId = DevHelper_Helper_ShippableHelper::getVersionId($class, $classFile, $classContents); - if ($classVersionId === false) { - die('Add-on class version could not be detected: ' . $classFile); - } - } - - $oursClass = 'DevHelper_Helper_' . substr($class, $strPos); - $oursFile = $this->_autoloaderClassToFile($oursClass); - $oursFile = $this->_locateClassFile($oursFile); - if (file_exists($oursFile)) { - $oursContents = file_get_contents($oursFile); - - $oursVersionId = DevHelper_Helper_ShippableHelper::getVersionId($oursClass, $oursFile, $oursContents); - if ($oursVersionId === false) { - die('DevHelper class version could not be detected: ' . $oursFile); - } - } else { - die('DevHelper file could not be found: ' . $oursFile); - } - - if ($classVersionId < $oursVersionId) { - if (!DevHelper_Helper_ShippableHelper::update($class, $classFile, $oursClass, $oursContents)) { - die('Add-on file could not be updated: ' . $classFile); - } - } - } - - return $classFile; - } - - public function getRootDir() - { - return $this->_rootDir; - } - - protected function _autoloaderClassToFile($class) - { - if (preg_match('#[^a-zA-Z0-9_\\\\]#', $class)) { - return false; - } - return $this->_rootDir . '/' . str_replace(array('_', '\\'), '/', $class) . '.php'; - } - - protected function _locateClassFile($classFile) - { - if (!empty($_SERVER['DEVHELPER_ROUTER_PHP'])) { - return DevHelper_Router::locateCached($classFile); - } - - return $classFile; - } -} diff --git a/library/DevHelper/Config/Base.php b/library/DevHelper/Config/Base.php deleted file mode 100755 index 3470ea5..0000000 --- a/library/DevHelper/Config/Base.php +++ /dev/null @@ -1,415 +0,0 @@ -_upgrade(); - - return empty($result); - } - - public function getDataClasses() - { - return $this->_dataClasses; - } - - public function getDataClass($name) - { - $name = $this->_normalizeDbName($name); - - if (!$this->checkDataClassExists($name)) { - return array(); - } - - $dataClass = $this->_dataClasses[$name]; - - foreach ($dataClass['files'] as &$file) { - if (!empty($file)) { - $path = DevHelper_Generator_File::getClassPath($file['className'], $this); - $hash = DevHelper_Generator_File::calcHash($path); - if ($hash != $file['hash']) { - $file['changed'] = true; - } - } - } - - return $dataClass; - } - - public function addDataClass( - $name, - $fields = array(), - $primaryKey = false, - $indeces = array(), - $extraData = array() - ) { - $name = $this->_normalizeDbName($name); - - $this->_dataClasses[$name] = array( - 'name' => $name, - 'camelCase' => DevHelper_Generator_File::getCamelCase($name), - 'camelCasePlural' => false, - 'camelCaseWSpace' => ucwords(str_replace('_', ' ', $name)), - 'camelCasePluralWSpace' => false, - 'fields' => array(), - 'phrases' => array(), - 'title_field' => false, - 'primaryKey' => false, - 'indeces' => array(), - - 'files' => array( - 'data_writer' => false, - 'model' => false, - 'route_prefix_admin' => false, - 'controller_admin' => false, - ), - ); - - foreach ($extraData as $key => $value) { - $this->_dataClasses[$name][$key] = $value; - } - - foreach ($fields as $fieldName => $fieldInfo) { - $fieldInfo = array_merge(array('name' => $fieldName), $fieldInfo); - $this->addDataClassField($name, $fieldInfo); - } - $this->addDataClassPrimaryKey($name, $primaryKey); - foreach ($indeces as $index) { - $this->addDataClassIndex($name, $index); - } - - return true; - } - - public function addDataClassField($name, array $field) - { - $name = $this->_normalizeDbName($name); - $field['name'] = $this->_normalizeDbName($field['name']); - $field['type'] = strtolower($field['type']); - if (!in_array($field['type'], DevHelper_Generator_Db::getDataTypes())) { - $field['type'] = XenForo_DataWriter::TYPE_SERIALIZED; - } - - if (empty($this->_dataClasses[$name]['title_field']) - && in_array( - $field['type'], - array(XenForo_DataWriter::TYPE_STRING) - ) - ) { - $this->_dataClasses[$name]['title_field'] = $field['name']; - } - - $this->_dataClasses[$name]['fields'][$field['name']] = $field; - - return true; - } - - public function addDataClassPrimaryKey($name, $fields) - { - $name = $this->_normalizeDbName($name); - - if (!is_array($fields)) { - $fields = array($fields); - } - - $primaryKey = array(); - - foreach ($fields as $field) { - $field = $this->_normalizeDbName($field); - if (!$this->checkDataClassFieldExists($name, $field)) { - return false; - } - $primaryKey[] = $field; - } - - if (!empty($primaryKey)) { - $this->_dataClasses[$name]['primaryKey'] = $primaryKey; - return true; - } - - return false; - } - - public function addDataClassIndex($name, array $index) - { - $name = $this->_normalizeDbName($name); - $fields = array(); - - if (!is_array($index['fields'])) { - $index['fields'] = array($index['fields']); - } - - foreach ($index['fields'] as $field) { - $field = $this->_normalizeDbName($field); - if ($this->checkDataClassFieldExists($name, $field)) { - $fields[] = $field; - } else { - return false; - } - } - if (empty($fields)) { - return false; - } - - if (isset($index['name'])) { - $indexName = $index['name']; - } else { - $indexName = implode('_', $fields); - } - - $type = !empty($index['type']) ? $index['type'] : 'NORMAL'; - if (!in_array(strtoupper($type), DevHelper_Generator_Db::getIndexTypes())) { - $type = 'NORMAL'; - } - - $this->_dataClasses[$name]['indeces'][$indexName] = array( - 'name' => $indexName, - 'fields' => $fields, - 'type' => strtoupper($type), - ); - - return true; - } - - public function updateDataClassFile($name, $fileType, $className, $path) - { - $name = $this->_normalizeDbName($name); - - $this->_dataClasses[$name]['files'][$fileType] = array( - 'className' => $className, - 'hash' => DevHelper_Generator_File::calcHash($path), - ); - } - - public function checkDataClassExists($name) - { - $name = $this->_normalizeDbName($name); - - return isset($this->_dataClasses[$name]); - } - - public function checkDataClassFieldExists($name, $field) - { - $name = $this->_normalizeDbName($name); - $field = $this->_normalizeDbName($field); - - return isset($this->_dataClasses[$name]['fields'][$field]); - } - - public function getDataPatches() - { - return $this->_dataPatches; - } - - public function addDataPatch($table, array $patch) - { - if (!empty($patch['index'])) { - if (!isset($patch['name'])) { - $patch['name'] = implode('_', $patch['fields']); - } - $patchKey = 'index::' . $patch['name']; - - if (isset($patch['type'])) { - $patch['type'] = strtoupper($patch['type']); - } else { - $patch['type'] = ''; - } - if (!in_array($patch['type'], DevHelper_Generator_Db::getIndexTypes())) { - $patch['type'] = 'NORMAL'; - } - - if (!isset($patch['fields'])) { - throw new XenForo_Exception('addDataPatch(index=true) requires `fields`'); - } - if (!is_array($patch['fields'])) { - $patch['fields'] = array(strval($patch['fields'])); - } - } else { - $patch['name'] = DevHelper_Generator_Db::getFieldName($this, $this->_normalizeDbName($patch['name'])); - $patchKey = $patch['name']; - - if (!isset($patch['type'])) { - throw new XenForo_Exception('addDataPatch() requires `type`'); - } - $patch['type'] = strtolower($patch['type']); - if (!in_array($patch['type'], DevHelper_Generator_Db::getDataTypes())) { - $patch['type'] = XenForo_DataWriter::TYPE_SERIALIZED; - } - } - - $this->_dataPatches[$table][$patchKey] = $patch; - - return true; - } - - public function setExportPath($path) - { - if (is_dir($path) AND is_writable($path)) { - $this->_exportPath = $path; - } else { - die('EXPORT PATH IS NOT WRITABLE'); - } - } - - public function getExportPath() - { - $path = $this->_exportPath; - - if (!is_string($path)) { - $path = ''; - } - - if (isset($_SERVER['DEVHELPER_ROUTER_PHP'])) { - $path = sprintf( - '%s/xenforo/data/exported/%s', - dirname($_SERVER['DEVHELPER_ROUTER_PHP']), - str_replace('_', DIRECTORY_SEPARATOR, $this->getClassPrefix()) - ); - XenForo_Helper_File::createDirectory($path); - } - - if ($path === '') { - // fallback - $path = DevHelper_Generator_File::getAddOnIdPath($this); - } - - if (!is_dir($path) || !is_writable($path)) { - die(sprintf('Export path (%s) is not writable', $path)); - } - - return $path; - } - - public function getExportIncludes() - { - return $this->_exportIncludes; - } - - public function getExportExcludes() - { - return $this->_exportExcludes; - } - - public function getExportAddOns() - { - return $this->_exportAddOns; - } - - public function getExportStyles() - { - return $this->_exportStyles; - } - - public function getPrefix() - { - if (!empty($this->_options['prefix'])) { - return $this->_options['prefix']; - } - - $configClassName = get_class($this); - $parts = explode('_', $configClassName); - array_pop($parts); - array_pop($parts); - $prefix = implode('_', $parts); - - return $prefix; - } - - public function getClassPrefix() - { - if (!empty($this->_options['classPrefix'])) { - return $this->_options['classPrefix']; - } - - return $this->getPrefix(); - } - - public function outputSelf() - { - $className = get_class($this); - - $dataClasses = DevHelper_Generator_File::varExport($this->_dataClasses); - $dataPatches = DevHelper_Generator_File::varExport($this->_dataPatches); - $exportPath = DevHelper_Generator_File::varExport($this->_exportPath); - $exportIncludes = DevHelper_Generator_File::varExport($this->_exportIncludes); - $exportExcludes = DevHelper_Generator_File::varExport($this->_exportExcludes); - $exportAddOns = DevHelper_Generator_File::varExport($this->_exportAddOns); - $exportStyles = DevHelper_Generator_File::varExport($this->_exportStyles); - $options = DevHelper_Generator_File::varExport($this->_options); - - $contents = <<addDataClass( - 'name_here', - array( // fields - 'field_here' => array( - 'type' => 'type_here', - // 'length' => 'length_here', - // 'required' => true, - // 'allowedValues' => array('value_1', 'value_2'), - // 'default' => 0, - // 'autoIncrement' => true, - ), - // other fields go here - ), - array('primary_key_1', 'primary_key_2'), // or 'primary_key', both are okie - array( // indeces - array( - 'fields' => array('field_1', 'field_2'), - 'type' => 'NORMAL', // UNIQUE or FULLTEXT - ), - ), - ); - */ - } -} -EOF; - - return $contents; - } - - protected function _normalizeDbName($name) - { - return $this->_normalizeName($name); - } - - protected function _normalizeName($name) - { - return preg_replace('/[^a-zA-Z_]/', '', $name); - } -} diff --git a/library/DevHelper/ControllerHelper/AddOn.php b/library/DevHelper/ControllerHelper/AddOn.php deleted file mode 100644 index 894ff69..0000000 --- a/library/DevHelper/ControllerHelper/AddOn.php +++ /dev/null @@ -1,65 +0,0 @@ -Sync Now.', - XenForo_Link::buildAdminLink('tools/dev/helper/sync') - )); - } - } - - public function filterKeepActiveAddOns(array &$dataGrouped, array $addOns = null) - { - $removedCount = 0; - - if ($addOns === null) { - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = $this->_controller->getModelFromCache('XenForo_Model_AddOn'); - $addOns = $addOnModel->getAllAddOns(); - } - - foreach ($addOns as $addOn) { - if (empty($addOn['active'])) { - // remove template modifications from inactive add-ons - if (!empty($dataGrouped[$addOn['addon_id']])) { - $removedCount += count($dataGrouped[$addOn['addon_id']]); - unset($dataGrouped[$addOn['addon_id']]); - } - } - } - - return $removedCount; - } - - public function filterKeepActiveAddOnsDirect(array &$data, array $addOns = null) - { - $removedCount = 0; - - if ($addOns === null) { - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = $this->_controller->getModelFromCache('XenForo_Model_AddOn'); - $addOns = $addOnModel->getAllAddOns(); - } - - foreach (array_keys($data) as $dataId) { - $singleRef = &$data[$dataId]; - - if (empty($addOns[$singleRef['addon_id']])) { - continue; - } - $addOnRef = &$addOns[$singleRef['addon_id']]; - - if (empty($addOnRef['active'])) { - $removedCount++; - unset($data[$dataId]); - } - } - - return $removedCount; - } -} diff --git a/library/DevHelper/Generator/Code/Common.php b/library/DevHelper/Generator/Code/Common.php deleted file mode 100755 index b4d5db9..0000000 --- a/library/DevHelper/Generator/Code/Common.php +++ /dev/null @@ -1,233 +0,0 @@ -_className === false) { - throw new Exception('_setClassName() must be called before calling _generate()'); - } - - $output = ''; - - $output .= "_className}"; - if ($this->_baseClass !== false) { - $output .= " extends {$this->_baseClass}"; - } - if (!empty($this->_interfaces)) { - $isFirstInterface = true; - $output .= " implements"; - foreach ($this->_interfaces as $interface) { - if ($isFirstInterface) { - $isFirstInterface = false; - } else { - $output .= ","; - } - $output .= " {$interface}"; - } - } - $output .= "\n{\n"; - - $output .= "\n " . DevHelper_Generator_File::COMMENT_AUTO_GENERATED_START . "\n"; - - if (!empty($this->_constants)) { - $output .= "\n"; - foreach ($this->_constants as $constantName => $constantValue) { - $output .= " const {$constantName} = {$constantValue};\n"; - } - } - - if (!empty($this->_properties)) { - $output .= "\n"; - foreach ($this->_properties as $propertyName => $propertyDeclare) { - if ($propertyDeclare != null) { - $output .= " {$propertyDeclare};\n"; - } else { - $output .= " protected {$propertyName};\n"; - } - } - } - - if (!empty($this->_methods)) { - foreach ($this->_methods as $method) { - $output .= "\n"; - $output .= $this->_generateMethod($method); - } - } - - $output .= "\n " . DevHelper_Generator_File::COMMENT_AUTO_GENERATED_END . "\n"; - - if (!empty($this->_customizableMethods)) { - foreach ($this->_customizableMethods as $method) { - $output .= "\n"; - $output .= $this->_generateMethod($method); - } - } - - $output .= "\n"; - - // class ClassName { - $output .= "}\n"; - - return $output; - } - - protected function _generateMethod($method, $level = 1) - { - $output = ""; - $indentation = str_repeat(" ", $level); - - $output .= $indentation; - if ($method['visibility'] != '') { - $output .= "{$method['visibility']} "; - } - $output .= "function {$method['name']}("; - - $isFirstParam = true; - foreach ($method['params'] as $paramName => $paramDeclare) { - if ($isFirstParam) { - $isFirstParam = false; - } else { - $output .= ", "; - } - - if ($paramDeclare != null) { - $output .= "{$paramDeclare}"; - } else { - $output .= "{$paramName}"; - } - } - - $output .= ")\n {\n"; - - $codeBlocks = $method['code']; - ksort($codeBlocks); - $isFirstCodeBlock = true; - foreach ($codeBlocks as $codeBlock) { - $lines = explode("\n", $codeBlock); - $codeBlockOutput = ''; - $foundNonEmptyLine = false; - - foreach ($lines as $line) { - $trimmed = trim($line); - - if (strlen($trimmed) == 0) { - // this is an empty line, only output if we have found some non-empty line before - if ($foundNonEmptyLine) { - $codeBlockOutput .= "\n"; - } - } else { - $foundNonEmptyLine = true; - $codeBlockOutput .= "{$indentation} {$line}\n"; - } - } - - // remove the last empty lines - $codeBlockOutput = rtrim($codeBlockOutput); - - if ($isFirstCodeBlock) { - $isFirstCodeBlock = false; - } else { - $output .= "\n"; - } - $output .= $codeBlockOutput . "\n"; - } - - $output .= "{$indentation}}\n"; - - return $output; - } - - protected function _setClassName($className) - { - $this->_className = $className; - } - - protected function _setBaseClass($className) - { - $this->_baseClass = $className; - } - - protected function _addInterface($interface) - { - $this->_interfaces[] = $interface; - } - - protected function _addConstant($name, $value) - { - $this->_constants[$name] = $value; - } - - protected function _addProperty($name, $declare = null) - { - $this->_properties[$name] = $declare; - } - - protected function _addMethod($name, $visibility, array $params, $code, $codeId = null) - { - $this->_addMethodCommon($this->_methods, $name, $visibility, $params); - - // switch code with existing code (detected by codeId) if needed - if (!empty($code)) { - if ($codeId !== null) { - $this->_methods[$name]['code'][$codeId] = $code; - } else { - $this->_methods[$name]['code'][] = $code; - } - } - } - - protected function _addCustomizableMethod($name, $visibility, array $params) - { - $this->_addMethodCommon($this->_customizableMethods, $name, $visibility, $params); - - $this->_customizableMethods[$name]['code'] = array('// customized code goes here'); - } - - protected function _addMethodCommon(array &$methods, $name, $visibility, $params) - { - if (!isset($methods[$name])) { - $methods[$name] = array( - 'name' => $name, - 'visibility' => '', - 'params' => array(), - 'code' => array(), - ); - } - - // we have to use the broader visibility between - static $visibilities = array( - '', - 'private', - 'protected', - 'public', - 'protected static', - 'public static' - ); - $oldVisibilityLevel = array_search($methods[$name]['visibility'], $visibilities); - $newVisibilityLevel = array_search($visibility, $visibilities); - $max = max(array( - $oldVisibilityLevel, - $newVisibilityLevel - )); - $methods[$name]['visibility'] = $visibilities[$max]; - - foreach ($params as $paramName => $paramDeclare) { - if (is_numeric($paramName)) { - $paramName = $paramDeclare; - $paramDeclare = null; - } - $methods[$name]['params'][$paramName] = $paramDeclare; - } - } -} diff --git a/library/DevHelper/Generator/Code/ControllerAdmin.php b/library/DevHelper/Generator/Code/ControllerAdmin.php deleted file mode 100755 index 9d13a9d..0000000 --- a/library/DevHelper/Generator/Code/ControllerAdmin.php +++ /dev/null @@ -1,724 +0,0 @@ -_addOn = $addOn; - $this->_config = $config; - $this->_dataClass = $dataClass; - $this->_info = $info; - } - - protected function _generate() - { - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate %s: too many fields in primary key', - $this->_info['controller'] - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - $variableName = DevHelper_Generator_Code_Model::getVariableName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $variableNamePlural = DevHelper_Generator_Code_Model::getVariableNamePlural( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $modelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $modelGetFunctionName = DevHelper_Generator_Code_Model::generateGetDataFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $dataWriterClassName = DevHelper_Generator_Code_DataWriter::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $viewListClassName = $this->_getViewClassName('list'); - $viewEditClassName = $this->_getViewClassName('edit'); - $viewDeleteClassName = $this->_getViewClassName('delete'); - - $imageField = DevHelper_Generator_Db::getImageField($this->_dataClass['fields']); - list($templateList, $templateEdit, $templateDelete) = $this->_generateTemplates( - $variableName, - $variableNamePlural, - $imageField - ); - - $this->_setClassName($this->_info['controller']); - $this->_setBaseClass('XenForo_ControllerAdmin_Abstract'); - - $actionIndexConditions = array(); - $actionIndexFetchOptions = array(); - - if (isset($this->_dataClass['fields']['display_order'])) { - $actionIndexFetchOptions['order'] = 'display_order'; - } - - $actionIndexConditions = DevHelper_Generator_File::varExport($actionIndexConditions); - $actionIndexFetchOptions = DevHelper_Generator_File::varExport($actionIndexFetchOptions); - - $this->_addMethod('actionIndex', 'public', array(), " - -\$conditions = {$actionIndexConditions}; -\$fetchOptions = {$actionIndexFetchOptions}; - -\${$variableName}Model = \$this->_get{$this->_dataClass['camelCase']}Model(); -\${$variableNamePlural} = \${$variableName}Model->{$modelGetFunctionName}(\$conditions, \$fetchOptions); - -\$viewParams = array( - '{$variableNamePlural}' => \${$variableNamePlural}, -); - -return \$this->responseView('$viewListClassName', '$templateList', \$viewParams); - "); - - $this->_addMethod('actionAdd', 'public', array(), " - -\$viewParams = array( - '$variableName' => array(), -); - - ", '000'); - - $this->_addMethod('actionAdd', 'public', array(), " - -return \$this->responseView('$viewEditClassName', '$templateEdit', \$viewParams); - - ", '999'); - - $this->_addMethod('actionEdit', 'public', array(), " - -\$id = \$this->_input->filterSingle('{$idField}', XenForo_Input::UINT); -\${$variableName} = \$this->_get{$this->_dataClass['camelCase']}OrError(\$id); - -\$viewParams = array( - '$variableName' => \${$variableName}, -); - - ", '000'); - - $this->_addMethod('actionEdit', 'public', array(), " - -return \$this->responseView('$viewEditClassName', '$templateEdit', \$viewParams); - - ", '999'); - - $this->_addMethod('actionSave', 'public', array(), " - -\$this->_assertPostOnly(); - -\$id = \$this->_input->filterSingle('{$idField}', XenForo_Input::UINT); -\$dw = \$this->_get{$this->_dataClass['camelCase']}DataWriter(); -if (\$id) { - \$dw->setExistingData(\$id); -} - - ", '000'); - - $this->_addMethod('actionSave', 'public', array(), " - -\$this->_prepareDwBeforeSaving(\$dw); - -\$dw->save(); - -return \$this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('{$this->_info['routePrefix']}') -); - - ", '999'); - - $this->_addMethod('actionDelete', 'public', array(), " - -\$id = \$this->_input->filterSingle('{$idField}', XenForo_Input::UINT); -\${$variableName} = \$this->_get{$this->_dataClass['camelCase']}OrError(\$id); - -if (\$this->isConfirmedPost()) { - \$dw = \$this->_get{$this->_dataClass['camelCase']}DataWriter(); - \$dw->setExistingData(\$id); - \$dw->delete(); - - return \$this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('{$this->_info['routePrefix']}') - ); -} else { - \$viewParams = array( - '$variableName' => \${$variableName}, - ); - - return \$this->responseView('$viewDeleteClassName', '$templateDelete', \$viewParams); -} - - "); - - $phraseNotFound = $this->_getPhraseName('_not_found'); - $this->_addMethod("_get{$this->_dataClass['camelCase']}OrError", 'protected', array( - '$id', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\${$variableName} = \$this->_get{$this->_dataClass['camelCase']}Model()->get{$this->_dataClass['camelCase']}ById(\$id, \$fetchOptions); - -if (empty(\${$variableName})) { - throw \$this->responseException(\$this->responseError(new XenForo_Phrase('$phraseNotFound'), 404)); -} - -return \${$variableName}; - - "); - - $this->_addMethod("_get{$this->_dataClass['camelCase']}Model", 'protected', array(), " - -return \$this->getModelFromCache('$modelClassName'); - - "); - - $this->_addMethod(" _get{$this->_dataClass['camelCase']}DataWriter", 'protected', array(), " - -return XenForo_DataWriter::create('$dataWriterClassName'); - - "); - - $this->_addCustomizableMethod( - '_prepareDwBeforeSaving', - 'protected', - array('$dw' => "$dataWriterClassName \$dw") - ); - - return parent::_generate(); - } - - protected function _generateTemplates($variableName, $variableNamePlural, $imageField) - { - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate %s: too many fields in primary key', - $this->_info['controller'] - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - $dataTitle = "\${$variableName}" - . ( - empty($this->_dataClass['title_field']) - ? (".{$idField}") - : ( - is_array($this->_dataClass['title_field']) - ? (".{$this->_dataClass['title_field'][0]}.{$this->_dataClass['title_field'][1]}") - : (".{$this->_dataClass['title_field']}") - )); - - // create the phrases - $phraseClassName = $this->_getPhraseName(''); - $phrasePlural = $this->_getPhrasePluralName(); - $phraseAdd = $this->_getPhraseName('_add'); - $phraseEdit = $this->_getPhraseName('_edit'); - $phraseDelete = $this->_getPhraseName('_delete'); - $phraseSave = $this->_getPhraseName('_save'); - $phraseConfirmDeletion = $this->_getPhraseName('_confirm_deletion'); - $phrasePleaseConfirm = $this->_getPhraseName('_please_confirm'); - $phraseNotFound = $this->_getPhraseName('_not_found'); - $phraseNoResults = $this->_getPhraseName('_no_results'); - - $this->_generatePhrase($phraseClassName, $this->_dataClass['camelCaseWSpace']); - if (!empty($this->_dataClass['camelCasePluralWSpace'])) { - $this->_generatePhrase($phrasePlural, $this->_dataClass['camelCasePluralWSpace']); - } else { - $this->_generatePhrase($phrasePlural, $this->_dataClass['camelCaseWSpace'] . ' (Plural)'); - } - $this->_generatePhrase($phraseAdd, 'Add New ' . $this->_dataClass['camelCaseWSpace']); - $this->_generatePhrase($phraseEdit, 'Edit ' . $this->_dataClass['camelCaseWSpace']); - $this->_generatePhrase($phraseDelete, 'Delete ' . $this->_dataClass['camelCaseWSpace']); - $this->_generatePhrase($phraseSave, 'Save ' . $this->_dataClass['camelCaseWSpace']); - $this->_generatePhrase($phraseConfirmDeletion, 'Confirm Deletion of ' . $this->_dataClass['camelCaseWSpace']); - $this->_generatePhrase( - $phrasePleaseConfirm, - 'Please confirm that you want to delete the following ' . strtolower($this->_dataClass['camelCaseWSpace']) - ); - $this->_generatePhrase( - $phraseNotFound, - 'The requested ' . strtolower($this->_dataClass['camelCaseWSpace']) . ' could not be found' - ); - $this->_generatePhrase( - $phraseNoResults, - 'No ' . strtolower($this->_dataClass['camelCaseWSpace']) . ' could be found...' - ); - // finished creating pharses - - $templateList = $this->_getTemplateTitle('_list'); - $templateListItems = $this->_getTemplateTitle('_list_items'); - $templateEdit = $this->_getTemplateTitle('_edit'); - $templateDelete = $this->_getTemplateTitle('_delete'); - - // create the templates - $listItemExtras = ''; - if ($imageField !== false) { - $listItemExtras .= << - {xen:if '{\${$variableName}.images.s}', ''} - -EOF; - } - - $templateListItemsTemplate = << - - {{$dataTitle}}{$listItemExtras} - - -EOF; - $this->_generateAdminTemplate($templateListItems, $templateListItemsTemplate); - // finished template_list_items - - $templateListTemplate = <<{xen:phrase $phrasePlural} - - - + {xen:phrase $phraseAdd} - - - - - - - -

- - {xen:phrase $phrasePlural} -

- -
    - -
- - - -
{xen:phrase $phraseNoResults}
-
-
-EOF; - $this->_generateAdminTemplate($templateList, $templateListTemplate); - // finished template_list - - $templateEditFormExtra = 'class="AutoValidator" data-redirect="yes"'; - $templateEditFields = ''; - $extraViewParamsForTemplateEdit = array(); - $filterParams = array(); - - if (!empty($this->_dataClass['phrases'])) { - foreach ($this->_dataClass['phrases'] as $phraseType) { - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $this->_dataClass['name'] . '_' . $phraseType - ); - - $templateEditFields .= << -EOF; - } - } - - foreach ($this->_dataClass['fields'] as $field) { - if ($field['name'] == $idField) { - continue; - } - if (empty($field['required'])) { - // ignore non-required fields - continue; - } - if ($field['name'] == $imageField) { - // ignore image field - continue; - } - - // queue this field for validation - $filterParams[$field['name']] = $field['type']; - - if ($field['name'] == $this->_dataClass['title_field']) { - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $field['name'] - ); - $templateEditFields .= << -EOF; - continue; - } - - if (substr($field['name'], -3) == '_id') { - // link to another dataClass? - $other = substr($field['name'], 0, -3); - if ($this->_config->checkDataClassExists($other)) { - // yeah! - $otherDataClass = $this->_config->getDataClass($other); - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $otherDataClass, - $otherDataClass['name'] - ); - $templateEditFields .= << -   - - -EOF; - $otherDataClassModelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $otherDataClass - ); - $extraViewParamsForTemplateEdit["list{$otherDataClass['camelCase']}"] = - "\$viewParams['list{$otherDataClass['camelCase']}'] = " . - "\$this->getModelFromCache('{$otherDataClassModelClassName}')->getList();"; - continue; - } elseif ($other === $this->_dataClass['name'] . '_parent') { - // just a parent of this same class - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $other - ); - $templateEditFields .= << -   - - -EOF; - $thisDataClassModelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $extraViewParamsForTemplateEdit["list{$this->_dataClass['camelCase']}"] = - "\$viewParams['list{$this->_dataClass['camelCase']}'] = " . - "\$this->getModelFromCache('{$thisDataClassModelClassName}')->getList();"; - continue; - } - } - - if ($field['name'] == 'display_order') { - // special case with display_order - $templateEditFields .= << -EOF; - continue; - } - - if ($field['type'] == 'string' && !empty($field['allowedValues'])) { - // render field with enum type as a list of selections - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $field['name'] - ); - - $templateEditFields .= << -   -EOF; - - foreach ($field['allowedValues'] as $allowedValue) { - $allowedValuePhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $field['name'] . '_' . $allowedValue - ); - - $templateEditFields .= <<{xen:phrase $allowedValuePhraseName} -EOF; - } - - $templateEditFields .= << -EOF; - - continue; - } - - if ($field['type'] == 'uint' AND substr($field['name'], 0, 3) === 'is_') { - // special case with boolean fields - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $field['name'] - ); - - $templateEditFields .= << - - -EOF; - - continue; - } - - $fieldPhraseName = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $field['name'] - ); - $extra = ''; - if ($field['type'] == XenForo_DataWriter::TYPE_STRING AND (empty($field['length']) OR $field['length'] > 255)) { - $extra .= ' rows="5"'; - } - $templateEditFields .= << -EOF; - } - - if ($imageField !== false) { - $fieldPhraseImage = DevHelper_Generator_Phrase::generatePhraseAutoCamelCaseStyle( - $this->_addOn, - $this->_config, - $this->_dataClass, - 'image' - ); - $templateEditFormExtra = 'enctype="multipart/form-data"'; - - $templateEditFields .= << -
- - - {\$imageSizeCode} - - -
- -EOF; - } - - $templateEditTemplate = <<{xen:if '{\${$variableName}.{$idField}}', '{xen:phrase $phraseEdit}', '{xen:phrase $phraseAdd}'} - - - - $templateEditFields - - - - - - - -EOF; - - $this->_generateAdminTemplate($templateEdit, $templateEditTemplate); - - // add extra code for add, edit actions - if (!empty($extraViewParamsForTemplateEdit)) { - $this->_addMethod('actionAdd', 'public', array(), implode("\n", $extraViewParamsForTemplateEdit), '100'); - $this->_addMethod('actionEdit', 'public', array(), implode("\n", $extraViewParamsForTemplateEdit), '100'); - } - - // add input fields to action save - if (!empty($this->_dataClass['phrases'])) { - $phraseCode = "\$phrases = \$this->_input->filterSingle('_phrases', XenForo_Input::ARRAY_SIMPLE);\n"; - foreach ($this->_dataClass['phrases'] as $phraseType) { - $dataWriterClassName = DevHelper_Generator_Code_DataWriter::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $dataPhraseConstantName = DevHelper_Generator_Code_DataWriter::generateDataPhraseConstant( - $this->_addOn, - $this->_config, - $this->_dataClass, - $phraseType - ); - - $phraseCode .= "if (isset(\$phrases['{$phraseType}']))\n{\n"; - $phraseCode .= " \$dw->setExtraData({$dataWriterClassName}::{$dataPhraseConstantName}, \$phrases['{$phraseType}']);\n"; - $phraseCode .= "}\n"; - } - - $this->_addMethod('actionSave', 'public', array(), " - -// get phrases from input data -{$phraseCode} - - ", '100'); - } - - if (!empty($filterParams)) { - $filterParamsExported = DevHelper_Generator_File::varExport($filterParams, 1); - $this->_addMethod('actionSave', 'public', array(), " - -// get regular fields from input data -\$dwInput = \$this->_input->filter($filterParamsExported); -\$dw->bulkSet(\$dwInput); - - ", '200'); - } - - if ($imageField !== false) { - // add image to action save - $this->_addMethod('actionSave', 'public', array(), " - -// try to save the uploaded image if any -\$image = XenForo_Upload::getUploadedFile('image'); -if (!empty(\$image)) { - \$dw->setImage(\$image); -} - - ", '300'); - } - // finished template_edit - - $templateDeleteTemplate = <<{xen:phrase $phraseConfirmDeletion}: {{$dataTitle}} -{xen:phrase $phraseConfirmDeletion} - - - {{$dataTitle}} - - - - - - -

{xen:phrase $phrasePleaseConfirm}:

- {{$dataTitle}} - - - - -
-EOF; - $this->_generateAdminTemplate($templateDelete, $templateDeleteTemplate); - - // finished creating our templates - - return array( - $templateList, - $templateEdit, - $templateDelete, - ); - } - - protected function _getPhraseName($suffix) - { - return DevHelper_Generator_Phrase::getPhraseName( - $this->_addOn, - $this->_config, - $this->_dataClass, - $this->_dataClass['name'] . $suffix - ); - } - - protected function _getPhrasePluralName() - { - if (!empty($this->_dataClass['camelCasePluralWSpace'])) { - $phrase = strtolower(str_replace(' ', '_', $this->_dataClass['camelCasePluralWSpace'])); - } else { - $phrase = $this->_dataClass['name'] . '_plural'; - } - - return DevHelper_Generator_Phrase::getPhraseName($this->_addOn, $this->_config, $this->_dataClass, $phrase); - } - - protected function _generatePhrase($phraseName, $phraseText) - { - DevHelper_Generator_Phrase::generatePhrase($this->_addOn, $phraseName, $phraseText); - } - - protected function _getTemplateTitle($suffix) - { - return DevHelper_Generator_Template::getTemplateTitle( - $this->_addOn, - $this->_config, - $this->_dataClass, - $this->_dataClass['name'] . $suffix - ); - } - - protected function _generateAdminTemplate($templateTitle, $templateHtml) - { - DevHelper_Generator_Template::generateAdminTemplate($this->_addOn, $templateTitle, $templateHtml); - } - - protected function _getClassName() - { - return self::getClassName($this->_addOn, $this->_config, $this->_dataClass); - } - - protected function _getViewClassName($view) - { - $subClassName = 'ViewAdmin_' . $this->_dataClass['camelCase'] . '_' . ucwords($view); - - return DevHelper_Generator_File::getClassName($this->_addOn['addon_id'], $subClassName, $this->_config); - } - - public static function generate(array $addOn, DevHelper_Config_Base $config, array $dataClass, array $info) - { - $g = new self($addOn, $config, $dataClass, $info); - - return array( - $g->_getClassName(), - $g->_generate() - ); - } - - public static function getClassName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return DevHelper_Generator_File::getClassName( - $addOn['addon_id'], - 'ControllerAdmin_' . $dataClass['camelCase'], - $config - ); - } -} diff --git a/library/DevHelper/Generator/Code/DataWriter.php b/library/DevHelper/Generator/Code/DataWriter.php deleted file mode 100755 index 40ea5fc..0000000 --- a/library/DevHelper/Generator/Code/DataWriter.php +++ /dev/null @@ -1,395 +0,0 @@ -_addOn = $addOn; - $this->_config = $config; - $this->_dataClass = $dataClass; - } - - protected function _generate() - { - $className = $this->_getClassName(); - $tableName = DevHelper_Generator_Db::getTableName($this->_config, $this->_dataClass['name']); - $tableFields = $this->_dataClass['fields']; - foreach ($tableFields as &$field) { - unset($field['name']); - if (!empty($field['length'])) { - $field['maxLength'] = $field['length']; - unset($field['length']); - } - - $field['type'] = DevHelper_Generator_File::varExportConstantFromArray($field['type'], array( - 'XenForo_DataWriter::TYPE_BOOLEAN', - 'XenForo_DataWriter::TYPE_STRING', - 'XenForo_DataWriter::TYPE_BINARY', - 'XenForo_DataWriter::TYPE_INT', - 'XenForo_DataWriter::TYPE_UINT', - 'XenForo_DataWriter::TYPE_UINT_FORCED', - 'XenForo_DataWriter::TYPE_FLOAT', - 'XenForo_DataWriter::TYPE_SERIALIZED', - 'XenForo_DataWriter::TYPE_JSON', - 'XenForo_DataWriter::TYPE_UNKNOWN', - )); - } - $tableFields = DevHelper_Generator_File::varExport($tableFields, 1); - $primaryKey = DevHelper_Generator_File::varExport($this->_dataClass['primaryKey']); - $modelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $this->_setClassName($className); - $this->_setBaseClass('XenForo_DataWriter'); - - $this->_addMethod('_getFields', 'protected', array(), " - -return array( - '{$tableName}' => {$tableFields} -); - - "); - - if (count($this->_dataClass['primaryKey']) == 1) { - $idField = reset($this->_dataClass['primaryKey']); - $this->_addMethod('_getExistingData', 'protected', array('$data'), " - -if (!\$id = \$this->_getExistingPrimaryKey(\$data, '{$idField}')) { - return false; -} - -return array('$tableName' => \$this->_get{$this->_dataClass['camelCase']}Model()->get{$this->_dataClass['camelCase']}ById(\$id)); - - "); - } else { - $this->_addMethod('_getExistingData', 'protected', array('$data'), " - -throw new XenForo_Exception('{$className} does not support _getExistingData()'); - - "); - } - - $this->_addMethod('_getUpdateCondition', 'protected', array('$tableName'), " - -\$conditions = array(); - -foreach ($primaryKey as \$field) { - \$conditions[] = \$field . ' = ' . \$this->_db->quote(\$this->getExisting(\$field)); -} - -return implode(' AND ', \$conditions); - - "); - - $this->_addMethod("_get{$this->_dataClass['camelCase']}Model", 'protected', array(), " - -return \$this->getModelFromCache('$modelClassName'); - - "); - - $this->_generateImageCode(); - $this->_generatePhrasesCode(); - $this->_generateParentCode(); - - return parent::_generate(); - } - - protected function _generateImageCode() - { - $imageField = DevHelper_Generator_Db::getImageField($this->_dataClass['fields']); - if ($imageField === false) { - // no image field... - return false; - } - - $modelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $this->_addConstant('DATA_IMAGE_PREPARED', '\'imagePrepared\''); - $this->_addConstant('IMAGE_SIZE_ORIGINAL', '-1'); - $this->_addProperty('$imageQuality', 'public static $imageQuality = 85'); - - $this->_addMethod('setImage', 'public', array('$upload' => 'XenForo_Upload $upload'), " - -if (!\$upload->isValid()) { - throw new XenForo_Exception(\$upload->getErrors(), true); -} - -if (!\$upload->isImage()) { - throw new XenForo_Exception(new XenForo_Phrase('uploaded_file_is_not_valid_image'), true); -}; - -\$imageType = \$upload->getImageInfoField('type'); -if (!in_array(\$imageType, array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG))) { - throw new XenForo_Exception(new XenForo_Phrase('uploaded_file_is_not_valid_image'), true); -} - -\$this->setExtraData(self::DATA_IMAGE_PREPARED, \$this->_prepareImage(\$upload)); -\$this->set('{$imageField}', XenForo_Application::\$time); - - "); - - $this->_addMethod('_prepareImage', 'protected', array('$upload' => 'XenForo_Upload $upload'), " - -\$outputFiles = array(); -\$fileName = \$upload->getTempFile(); -\$imageType = \$upload->getImageInfoField('type'); -\$outputType = \$imageType; -\$width = \$upload->getImageInfoField('width'); -\$height = \$upload->getImageInfoField('height'); - -\$imageSizes = \$this->getImageSizes(); -reset(\$imageSizes); - -while (list(\$sizeCode, \$maxDimensions) = each(\$imageSizes)) { - \$newTempFile = tempnam(XenForo_Helper_File::getTempDir(), 'xfa'); - - if (\$maxDimensions == self::IMAGE_SIZE_ORIGINAL) { - copy(\$fileName, \$newTempFile); - } else { - \$image = XenForo_Image_Abstract::createFromFile(\$fileName, \$imageType); - if (!\$image) { - continue; - } - - \$image->thumbnailFixedShorterSide(\$maxDimensions); - if (\$image->getWidth() > \$maxDimensions OR \$image->getHeight() > \$maxDimensions) { - \$image->crop(0, 0, \$maxDimensions, \$maxDimensions); - } - - \$image->output(\$outputType, \$newTempFile, self::\$imageQuality); - unset(\$image); - } - - \$outputFiles[\$sizeCode] = \$newTempFile; -} - -if (count(\$outputFiles) != count(\$imageSizes)) { - foreach (\$outputFiles AS \$tempFile) { - if (\$tempFile != \$fileName) { - @unlink(\$tempFile); - } - } - - throw new XenForo_Exception('Non-image passed in to _prepareImage'); -} - -return \$outputFiles; - - "); - - $this->_addMethod('_moveImages', 'protected', array('$uploaded'), " - -if (is_array(\$uploaded)) { - \$data = \$this->getMergedData(); - foreach (\$uploaded as \$sizeCode => \$tempFile) { - \$filePath = {$modelClassName}::getImageFilePath(\$data, \$sizeCode); - \$directory = dirname(\$filePath); - - if (XenForo_Helper_File::createDirectory(\$directory, true)) { - XenForo_Helper_File::safeRename(\$tempFile, \$filePath); - } - } -} - - "); - - $this->_addMethod('_postSave', 'protected', array(), " - -\$uploaded = \$this->getExtraData(self::DATA_IMAGE_PREPARED); -if (\$uploaded) { - \$this->_moveImages(\$uploaded); - - if (\$this->isUpdate()) { - // removes old image - \$existingData = \$this->getMergedExistingData(); - foreach (array_keys(\$this->getImageSizes()) as \$sizeCode) { - \$filePath = {$modelClassName}::getImageFilePath(\$existingData, \$sizeCode); - @unlink(\$filePath); - } - } -} - - "); - - $this->_addMethod('_postDelete', 'protected', array(), " - -\$existingData = \$this->getMergedExistingData(); -foreach (array_keys(\$this->getImageSizes()) as \$sizeCode) { - \$filePath = {$modelClassName}::getImageFilePath(\$existingData, \$sizeCode); - @unlink(\$filePath); -} - - "); - - $this->_addMethod('getImageSizes', 'public', array(), " - -return array( - 'x' => self::IMAGE_SIZE_ORIGINAL, - 'l' => 96, - 'm' => 48, - 's' => 24 -); - - "); - - return true; - } - - protected function _generatePhrasesCode() - { - if (!empty($this->_dataClass['phrases'])) { - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate phrases code for %s: too many fields in primary key', - $this->_getClassName() - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - foreach ($this->_dataClass['phrases'] as $phraseType) { - $camelCase = ucwords(str_replace('_', ' ', $phraseType)); - $constantName = self::generateDataPhraseConstant( - $this->_addOn, - $this->_config, - $this->_dataClass, - $phraseType - ); - $modelClassName = DevHelper_Generator_Code_Model::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $getPhraseTitleFunction = DevHelper_Generator_Code_Model::generateGetPhraseTitleFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass, - $phraseType - ); - - $this->_addConstant($constantName, "'phrase{$camelCase}'"); - - $this->_addMethod('_postSave', 'protected', array(), " - -\$phrase{$camelCase} = \$this->getExtraData(self::{$constantName}); -if (\$phrase{$camelCase} !== null) { - \$this->_insertOrUpdateMasterPhrase({$modelClassName}::{$getPhraseTitleFunction}(\$this->get('{$idField}')), \$phrase{$camelCase}); -} - - "); - - $this->_addMethod('_postDelete', 'protected', array(), " - -\$this->_deleteMasterPhrase({$modelClassName}::{$getPhraseTitleFunction}(\$this->get('{$idField}'))); - - "); - } - } - } - - protected function _generateParentCode() - { - $parentField = DevHelper_Generator_Db::getParentField($this->_dataClass['name'], $this->_dataClass['fields']); - if ($parentField === false) { - // no parent field... - return; - } - - $displayOrderField = false; - $depthField = false; - $lftField = false; - $rgtField = false; - foreach ($this->_dataClass['fields'] as $field) { - if ($field['name'] == 'display_order') { - $displayOrderField = $field['name']; - } elseif ($field['name'] == 'depth') { - $depthField = $field['name']; - } elseif ($field['name'] == 'lft') { - $lftField = $field['name']; - } elseif ($field['name'] == 'rgt') { - $rgtField = $field['name']; - } - } - if (empty($displayOrderField) OR empty($depthField) OR empty($lftField) OR empty($rgtField)) { - // no hierarchy fields - return; - } - - $tableName = DevHelper_Generator_Db::getTableName($this->_config, $this->_dataClass['name']); - $rebuildStructureFunctionName = DevHelper_Generator_Code_Model::generateRebuildStructureFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $titleFieldPostSaveConditions = ''; - if (!empty($this->_dataClass['title_field']) AND !is_array($this->_dataClass['title_field'])) { - $titleFieldPostSaveConditions = "\n OR \$this->isChanged('{$this->_dataClass['title_field']}')"; - } - - $this->_addMethod('_postSave', 'protected', array(), " - -if (\$this->isInsert() - OR \$this->isChanged('{$displayOrderField}') - OR \$this->isChanged('{$parentField}'){$titleFieldPostSaveConditions} -) { - \$this->_get{$this->_dataClass['camelCase']}Model()->{$rebuildStructureFunctionName}(); -} - - "); - - $this->_addMethod('_postDelete', 'protected', array(), " - -\$this->_db->update('{$tableName}', - array('{$parentField}' => \$this->get('{$parentField}')), - '{$parentField} = ' . \$this->_db->quote(\$this->get('{$parentField}')) -); - -\$this->_get{$this->_dataClass['camelCase']}Model()->{$rebuildStructureFunctionName}(); - - "); - } - - protected function _getClassName() - { - return self::getClassName($this->_addOn, $this->_config, $this->_dataClass); - } - - public static function generate(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $g = new self($addOn, $config, $dataClass); - - return array( - $g->_getClassName(), - $g->_generate() - ); - } - - public static function getClassName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return DevHelper_Generator_File::getClassName( - $addOn['addon_id'], - 'DataWriter_' . $dataClass['camelCase'], - $config - ); - } - - public static function generateDataPhraseConstant( - array $addOn, - DevHelper_Config_Base $config, - array $dataClass, - $phraseType - ) { - return 'DATA_PHRASE_' . strtoupper($phraseType); - } -} diff --git a/library/DevHelper/Generator/Code/Installer.php b/library/DevHelper/Generator/Code/Installer.php deleted file mode 100755 index 69e8818..0000000 --- a/library/DevHelper/Generator/Code/Installer.php +++ /dev/null @@ -1,133 +0,0 @@ -getDataClasses(); - foreach ($dataClasses as $dataClass) { - $table = array(); - $table['createQuery'] = DevHelper_Generator_Db::createTable($config, $dataClass); - $table['dropQuery'] = DevHelper_Generator_Db::dropTable($config, $dataClass); - - $tables[$dataClass['name']] = $table; - } - $tables = DevHelper_Generator_File::varExport($tables); - - $patches = array(); - $dataPatches = $config->getDataPatches(); - foreach ($dataPatches as $table => $tablePatches) { - foreach ($tablePatches as $dataPatch) { - $patch = array(); - $patch['table'] = $table; - $patch['tableCheckQuery'] = DevHelper_Generator_Db::showTables($config, $table); - - if (!empty($dataPatch['index'])) { - $patch['index'] = $dataPatch['name']; - $patch['checkQuery'] = DevHelper_Generator_Db::showIndexes($config, $table, $dataPatch); - $patch['addQuery'] = DevHelper_Generator_Db::alterTableAddIndex($config, $table, $dataPatch); - $patch['dropQuery'] = DevHelper_Generator_Db::alterTableDropIndex($config, $table, $dataPatch); - } else { - $patch['field'] = $dataPatch['name']; - $patch['checkQuery'] = DevHelper_Generator_Db::showColumns($config, $table, $dataPatch); - $patch['addQuery'] = DevHelper_Generator_Db::alterTableAddColumn($config, $table, $dataPatch); - $patch['modifyQuery'] = DevHelper_Generator_Db::alterTableModifyColumn($config, $table, $dataPatch); - $patch['dropQuery'] = DevHelper_Generator_Db::alterTableDropColumn($config, $table, $dataPatch); - } - - $patches[] = $patch; - } - } - $patches = DevHelper_Generator_File::varExport($patches); - - $commentAutoGeneratedStart = DevHelper_Generator_File::COMMENT_AUTO_GENERATED_START; - $commentAutoGeneratedEnd = DevHelper_Generator_File::COMMENT_AUTO_GENERATED_END; - - $contents = <<query(\$table['createQuery']); - } - - foreach (self::\$_patches as \$patch) { - \$tableExisted = \$db->fetchOne(\$patch['tableCheckQuery']); - if (empty(\$tableExisted)) { - continue; - } - - \$existed = \$db->fetchOne(\$patch['checkQuery']); - if (empty(\$existed)) { - \$db->query(\$patch['addQuery']); - } elseif (!empty(\$patch['modifyQuery'])) { - \$db->query(\$patch['modifyQuery']); - } - } - - self::installCustomized(\$existingAddOn, \$addOnData); - } - - public static function uninstall() - { - \$db = XenForo_Application::get('db'); - - foreach (self::\$_patches as \$patch) { - \$tableExisted = \$db->fetchOne(\$patch['tableCheckQuery']); - if (empty(\$tableExisted)) { - continue; - } - - \$existed = \$db->fetchOne(\$patch['checkQuery']); - if (!empty(\$existed)) { - \$db->query(\$patch['dropQuery']); - } - } - - foreach (self::\$_tables as \$table) { - \$db->query(\$table['dropQuery']); - } - - self::uninstallCustomized(); - } - - $commentAutoGeneratedEnd - - public static function installCustomized(\$existingAddOn, \$addOnData) - { - // customized install script goes here - } - - public static function uninstallCustomized() - { - // customized uninstall script goes here - } - -} - -EOF; - - return array( - $className, - $contents - ); - } - - public static function getClassName(array $addOn, DevHelper_Config_Base $config) - { - return DevHelper_Generator_File::getClassName($addOn['addon_id'], 'Installer', $config); - } -} diff --git a/library/DevHelper/Generator/Code/Listener.php b/library/DevHelper/Generator/Code/Listener.php deleted file mode 100644 index 2a73270..0000000 --- a/library/DevHelper/Generator/Code/Listener.php +++ /dev/null @@ -1,111 +0,0 @@ -setImportMode(true); - $dw->bulkSet(array( - 'event_id' => 'file_health_check', - 'callback_class' => self::getClassName($addOn, $config), - 'callback_method' => 'file_health_check', - 'addon_id' => $addOn['addon_id'], - )); - return $dw->save(); - } - - return false; - } - - public static function generateLoadClass($realClazz, array $addOn, DevHelper_Config_Base $config) - { - $method = sprintf('load_class_%s', $realClazz); - if (strlen($method) > 50) { - $method = sprintf('load_class_%s', md5($realClazz)); - } - - $existingContents = self::_getClassContents($addOn, $config); - $existingMethods = DevHelper_Helper_Php::extractMethods($existingContents); - if (in_array($method, $existingMethods, true)) { - return $method; - } - - $ourClazz = sprintf('%s_%s', $config->getClassPrefix(), $realClazz); - $methodCode = <<_addOn = $addOn; - $this->_config = $config; - $this->_dataClass = $dataClass; - } - - protected function _generate() - { - $className = $this->_getClassName(); - $tableName = DevHelper_Generator_Db::getTableName($this->_config, $this->_dataClass['name']); - $idField = ''; - if (count($this->_dataClass['primaryKey']) == 1) { - $idField = reset($this->_dataClass['primaryKey']); - } - $getFunctionName = self::generateGetDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - $countFunctionName = self::generateCountDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - - $tableAlias = $this->_dataClass['name']; - if (in_array($tableAlias, array( - 'group', - 'join', - 'order' - ))) { - $tableAlias = '_' . $tableAlias; - } - - $variableName = self::getVariableName($this->_addOn, $this->_config, $this->_dataClass); - $variableNamePlural = self::getVariableNamePlural($this->_addOn, $this->_config, $this->_dataClass); - $conditionFields = DevHelper_Generator_Db::getConditionFields($this->_dataClass['fields']); - - $this->_setClassName($className); - $this->_setBaseClass('XenForo_Model'); - - $this->_addCustomizableMethod("_{$getFunctionName}Customized", 'protected', array( - 'array &$data', - 'array $fetchOptions', - )); - $this->_addCustomizableMethod( - "_prepare{$this->_dataClass['camelCase']}ConditionsCustomized", - 'protected', - array( - 'array &$sqlConditions', - 'array $conditions', - 'array $fetchOptions', - ) - ); - $this->_addCustomizableMethod( - "_prepare{$this->_dataClass['camelCase']}FetchOptionsCustomized", - 'protected', - array( - '&$selectFields', - '&$joinTables', - 'array $fetchOptions', - ) - ); - $this->_addCustomizableMethod( - "_prepare{$this->_dataClass['camelCase']}OrderOptionsCustomized", - 'protected', - array( - 'array &$choices', - 'array &$fetchOptions', - ) - ); - - if (!empty($idField)) { - $this->_addMethod('getList', 'public', array( - '$conditions' => 'array $conditions = array()', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\${$variableNamePlural} = \$this->{$getFunctionName}(\$conditions, \$fetchOptions); -\$list = array(); - -foreach (\${$variableNamePlural} as \$id => \${$variableName}) { - \$list[\$id] = \${$variableName}" . ( - empty($this->_dataClass['title_field']) ? - ("['{$idField}']") : - ( - is_array($this->_dataClass['title_field']) ? - ("['{$this->_dataClass['title_field'][0]}']['{$this->_dataClass['title_field'][1]}']") : - ("['{$this->_dataClass['title_field']}']") - ) - ) . "; -} - -return \$list; - - "); - - $this->_addMethod("get{$this->_dataClass['camelCase']}ById", 'public', array( - '$id', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\${$variableNamePlural} = \$this->{$getFunctionName}(array('{$idField}' => \$id), \$fetchOptions); - -return reset(\${$variableNamePlural}); - - "); - - $this->_addMethod("get{$this->_dataClass['camelCase']}IdsInRange", 'public', array( - '$start', - '$limit', - ), " - -\$db = \$this->_getDb(); - -return \$db->fetchCol(\$db->limit(' - SELECT {$idField} - FROM {$tableName} - WHERE {$idField} > ? - ORDER BY {$idField} -', \$limit), \$start); - - "); - } - - $this->_addMethod($getFunctionName, 'public', array( - '$conditions' => 'array $conditions = array()', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\$whereConditions = \$this->prepare{$this->_dataClass['camelCase']}Conditions(\$conditions, \$fetchOptions); - -\$orderClause = \$this->prepare{$this->_dataClass['camelCase']}OrderOptions(\$fetchOptions); -\$joinOptions = \$this->prepare{$this->_dataClass['camelCase']}FetchOptions(\$fetchOptions); -\$limitOptions = \$this->prepareLimitFetchOptions(\$fetchOptions); - -\${$variableNamePlural} = \$this->" . (!empty($idField) ? "fetchAllKeyed" : "_getDb()->fetchAll") . "(\$this->limitQueryResults(\" - SELECT {$tableAlias}.* - \$joinOptions[selectFields] - FROM `{$tableName}` AS {$tableAlias} - \$joinOptions[joinTables] - WHERE \$whereConditions - \$orderClause - \", \$limitOptions['limit'], \$limitOptions['offset'] -)" . (!empty($idField) ? ", '{$idField}'" : "") . "); - - ", '001'); - - $this->_addMethod($getFunctionName, 'public', array( - '$conditions' => 'array $conditions = array()', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\$this->_{$getFunctionName}Customized(\${$variableNamePlural}, \$fetchOptions); - -return \${$variableNamePlural}; - - ", '999'); - - $this->_addMethod($countFunctionName, 'public', array( - '$conditions' => 'array $conditions = array()', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\$whereConditions = \$this->prepare{$this->_dataClass['camelCase']}Conditions(\$conditions, \$fetchOptions); - -\$orderClause = \$this->prepare{$this->_dataClass['camelCase']}OrderOptions(\$fetchOptions); -\$joinOptions = \$this->prepare{$this->_dataClass['camelCase']}FetchOptions(\$fetchOptions); -\$limitOptions = \$this->prepareLimitFetchOptions(\$fetchOptions); - -return \$this->_getDb()->fetchOne(\" - SELECT COUNT(*) - FROM `{$tableName}` AS {$tableAlias} - \$joinOptions[joinTables] - WHERE \$whereConditions -\"); - - "); - - $this->_addMethod("prepare{$this->_dataClass['camelCase']}Conditions", 'public', array( - '$conditions' => 'array $conditions = array()', - '$fetchOptions' => 'array $fetchOptions = array()', - ), " - -\$sqlConditions = array(); -\$db = \$this->_getDb(); - - "); - - foreach ($conditionFields as $conditionField) { - $this->_addMethod("prepare{$this->_dataClass['camelCase']}Conditions", '', array(), " - -if (isset(\$conditions['{$conditionField}'])) { - if (is_array(\$conditions['{$conditionField}'])) { - if (!empty(\$conditions['{$conditionField}'])) { - // only use IN condition if the array is not empty (nasty!) - \$sqlConditions[] = \"{$tableAlias}.{$conditionField} IN (\" . \$db->quote(\$conditions['{$conditionField}']) . \")\"; - } - } else { - \$sqlConditions[] = \"{$tableAlias}.{$conditionField} = \" . \$db->quote(\$conditions['{$conditionField}']); - } -} - - "); - } - - $this->_addMethod("prepare{$this->_dataClass['camelCase']}Conditions", '', array(), " - -\$this->_prepare{$this->_dataClass['camelCase']}ConditionsCustomized(\$sqlConditions, \$conditions, \$fetchOptions); - -return \$this->getConditionsForClause(\$sqlConditions); - - "); - - $this->_addMethod( - "prepare{$this->_dataClass['camelCase']}FetchOptions", - 'public', - array('$fetchOptions' => 'array $fetchOptions = array()'), - " - -\$selectFields = ''; -\$joinTables = ''; - -\$this->_prepare{$this->_dataClass['camelCase']}FetchOptionsCustomized(\$selectFields, \$joinTables, \$fetchOptions); - -return array( - 'selectFields' => \$selectFields, - 'joinTables' => \$joinTables -); - - " - ); - - $orderChoices = array(); - if (isset($this->_dataClass['fields']['display_order'])) { - if (isset($this->_dataClass['fields']['lft'])) { - $orderChoices['display_order'] = sprintf('%s.lft', $tableAlias); - } else { - $orderChoices['display_order'] = sprintf('%s.display_order', $tableAlias); - } - } - $orderChoices = DevHelper_Generator_File::varExport($orderChoices); - - $this->_addMethod("prepare{$this->_dataClass['camelCase']}OrderOptions", 'public', array( - '$fetchOptions' => 'array $fetchOptions = array()', - '$defaultOrderSql' => '$defaultOrderSql = \'\'', - ), " - -\$choices = {$orderChoices}; - -\$this->_prepare{$this->_dataClass['camelCase']}OrderOptionsCustomized(\$choices, \$fetchOptions); - -return \$this->getOrderByClause(\$choices, \$fetchOptions, \$defaultOrderSql); - - "); - - $this->_generateImageCode(); - $this->_generatePhrasesCode(); - $this->_generateOptionsCode(); - $this->_generateParentCode(); - - return parent::_generate(); - } - - protected function _generateImageCode() - { - $imageField = DevHelper_Generator_Db::getImageField($this->_dataClass['fields']); - if ($imageField === false) { - // no image field... - return ''; - } - - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate image code for %s: too many fields in primary key', - $this->_getClassName() - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - $getFunctionName = self::generateGetDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - $variableName = self::getVariableName($this->_addOn, $this->_config, $this->_dataClass); - $variableNamePlural = self::getVariableNamePlural($this->_addOn, $this->_config, $this->_dataClass); - $dwClassName = DevHelper_Generator_Code_DataWriter::getClassName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $configPrefix = $this->_config->getPrefix(); - $imagePath = "{$configPrefix}/{$this->_dataClass['camelCase']}"; - $imagePath = strtolower($imagePath); - - $this->_addMethod($getFunctionName, '', array(), " - -// build image urls and make them ready for all the records -\$imageSizes = XenForo_DataWriter::create('{$dwClassName}')->getImageSizes(); -foreach (\${$variableNamePlural} as &\${$variableName}) { - \${$variableName}['images'] = array(); - if (!empty(\${$variableName}['{$imageField}'])) { - foreach (\$imageSizes as \$imageSizeCode => \$imageSize) { - \${$variableName}['images'][\$imageSizeCode] = \$this->getImageUrl(\${$variableName}, \$imageSizeCode); - } - } -} - - ", '100'); - - $this->_addMethod('getImageFilePath', 'public static', array( - sprintf('$%s', $variableName) => sprintf('array $%s', $variableName), - '$size' => '$size = \'l\'', - ), " - -\$internal = self::_getImageInternal(\${$variableName}, \$size); - -if (!empty(\$internal)) { - return XenForo_Helper_File::getExternalDataPath() . \$internal; -} else { - return ''; -} - - "); - - $this->_addMethod('getImageUrl', 'public static', array( - sprintf('$%s', $variableName) => sprintf('array $%s', $variableName), - '$size' => '$size = \'l\'', - ), " - -\$internal = self::_getImageInternal(\${$variableName}, \$size); - -if (!empty(\$internal)) { - return XenForo_Application::\$externalDataUrl . \$internal; -} else { - return ''; -} - - "); - - $this->_addMethod('_getImageInternal', 'protected static', array( - sprintf('$%s', $variableName) => sprintf('array $%s', $variableName), - '$size', - ), " - -if (empty(\${$variableName}['{$idField}']) OR empty(\${$variableName}['{$imageField}'])) { - return ''; -} - -return '/{$imagePath}/' . \${$variableName}['{$idField}'] . '_' . \${$variableName}['{$imageField}'] . strtolower(\$size) . '.jpg'; - - "); - - return true; - } - - protected function _generatePhrasesCode() - { - $variableName = self::getVariableName($this->_addOn, $this->_config, $this->_dataClass); - $variableNamePlural = self::getVariableNamePlural($this->_addOn, $this->_config, $this->_dataClass); - - if (!empty($this->_dataClass['phrases'])) { - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate phrases code for %s: too many fields in primary key', - $this->_getClassName() - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - $statements = ''; - - foreach ($this->_dataClass['phrases'] as $phraseType) { - $getPhraseTitleFunction = self::generateGetPhraseTitleFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass, - $phraseType - ); - $phraseTitlePrefix = DevHelper_Generator_Phrase::getPhraseName( - $this->_addOn, - $this->_config, - $this->_dataClass, - $this->_dataClass['name'] - ) . '_'; - - $this->_addMethod($getPhraseTitleFunction, 'public static', array('$id'), " - -return \"{$phraseTitlePrefix}{\$id}_{$phraseType}\"; - - "); - - $statements .= " '{$phraseType}' => new XenForo_Phrase(self::{$getPhraseTitleFunction}(\${$variableName}['{$idField}'])),\n"; - } - - $getFunctionName = self::generateGetDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - - $this->_addMethod($getFunctionName, '', array(), " - -// prepare the phrases -foreach (\${$variableNamePlural} as &\${$variableName}) { - \${$variableName}['phrases'] = array( -{$statements} ); -} - - ", '300'); - } - } - - protected function _generateOptionsCode() - { - $variableName = self::getVariableName($this->_addOn, $this->_config, $this->_dataClass); - $variableNamePlural = self::getVariableNamePlural($this->_addOn, $this->_config, $this->_dataClass); - $optionsFields = DevHelper_Generator_Db::getOptionsFields($this->_dataClass['fields']); - - if (!empty($optionsFields)) { - $statements = ''; - - foreach ($optionsFields as $optionsField) { - $statements .= " \${$variableName}['{$optionsField}'] = @unserialize(\${$variableName}['{$optionsField}']);\n"; - $statements .= " if (empty(\${$variableName}['{$optionsField}'])) \${$variableName}['{$optionsField}'] = array();\n"; - } - - $getFunctionName = self::generateGetDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - - $this->_addMethod($getFunctionName, '', array(), " - -// parse all the options fields -foreach (\${$variableNamePlural} as &\${$variableName}) { -{$statements}} - - ", '400'); - } - } - - protected function _generateParentCode() - { - $parentField = DevHelper_Generator_Db::getParentField($this->_dataClass['name'], $this->_dataClass['fields']); - if ($parentField === false) { - // no parent field... - return; - } - - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf( - 'Cannot generate parent code for %s: too many fields in primary key', - $this->_getClassName() - )); - } - $idField = reset($this->_dataClass['primaryKey']); - - $displayOrderField = false; - $depthField = false; - $lftField = false; - $rgtField = false; - $breadcrumbField = DevHelper_Generator_Db::getBreadcrumbField( - $this->_dataClass['name'], - $this->_dataClass['fields'] - ); - ; - foreach ($this->_dataClass['fields'] as $field) { - if ($field['name'] == 'display_order') { - $displayOrderField = $field['name']; - } elseif ($field['name'] == 'depth') { - $depthField = $field['name']; - } elseif ($field['name'] == 'lft') { - $lftField = $field['name']; - } elseif ($field['name'] == 'rgt') { - $rgtField = $field['name']; - } - } - if (empty($displayOrderField) OR empty($depthField) OR empty($lftField) OR empty($rgtField)) { - // no hierarchy fields - return; - } - - $tableName = DevHelper_Generator_Db::getTableName($this->_config, $this->_dataClass['name']); - $getFunctionName = self::generateGetDataFunctionName($this->_addOn, $this->_config, $this->_dataClass); - $variableName = self::getVariableName($this->_addOn, $this->_config, $this->_dataClass); - $variableNamePlural = self::getVariableNamePlural($this->_addOn, $this->_config, $this->_dataClass); - - $rebuildStructureFunctionName = self::generateRebuildStructureFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - $getStructureChangesFunctionName = '_getStructureChanges'; - $groupByParentsFunctionName = self::generateGroupByParentsFunctionName( - $this->_addOn, - $this->_config, - $this->_dataClass - ); - - $this->_addMethod($rebuildStructureFunctionName, 'public', array(), " - -\$grouped = \$this->{$groupByParentsFunctionName}(\$this->{$getFunctionName}(array(), array('order' => '{$displayOrderField}'))); - -\$db = \$this->_getDb(); -XenForo_Db::beginTransaction(\$db); - -\$changes = \$this->{$getStructureChangesFunctionName}(\$grouped); -foreach (\$changes AS \$id => \$oneChanges) { - \$db->update('{$tableName}', \$oneChanges, '{$idField} = ' . \$db->quote(\$id)); -} - -XenForo_Db::commit(\$db); - -return \$changes; - - "); - - $titleFieldBreadcrumb = ''; - if (!empty($this->_dataClass['title_field']) AND !is_array($this->_dataClass['title_field'])) { - $titleFieldBreadcrumb = "\n '{$this->_dataClass['title_field']}' => " . - "\${$variableName}['{$this->_dataClass['title_field']}'],"; - } - - $breadcrumbStatements = ''; - if (!empty($breadcrumbField)) { - $breadcrumbStatements = "\n if (\${$variableName}['category_breadcrumb'] != \$serializedBreadcrumb) {"; - $breadcrumbStatements .= "\n \$thisChanges['category_breadcrumb'] = \$serializedBreadcrumb;"; - $breadcrumbStatements .= "\n }"; - } - - $this->_addMethod($getStructureChangesFunctionName, 'protected', array( - '$grouped' => 'array $grouped', - '$parentId' => '$parentId = 0', - '$depth' => '$depth = 0', - '$startPosition' => '$startPosition = 1', - 'nextPosition' => '&$nextPosition = 0', - '$breadcrumb' => 'array $breadcrumb = array()', - ), " - -\$nextPosition = \$startPosition; - -if (!isset(\$grouped[\$parentId])) { - return array(); -} - -\$changes = array(); -\$serializedBreadcrumb = serialize(\$breadcrumb); - -foreach (\$grouped[\$parentId] AS \$id => \${$variableName}) { - \$left = \$nextPosition; - \$nextPosition++; - - \$thisBreadcrumb = \$breadcrumb + array( - \$id => array( - '{$idField}' => \$id,{$titleFieldBreadcrumb} - '{$parentField}' => \${$variableName}['{$parentField}'], - ) - ); - - \$changes += \$this->{$getStructureChangesFunctionName}( - \$grouped, - \$id, - \$depth + 1, - \$nextPosition, - \$nextPosition, - \$thisBreadcrumb - ); - - \$thisChanges = array(); - if (\${$variableName}['depth'] != \$depth) { - \$thisChanges['depth'] = \$depth; - } - if (\${$variableName}['lft'] != \$left) { - \$thisChanges['lft'] = \$left; - } - if (\${$variableName}['rgt'] != \$nextPosition) { - \$thisChanges['rgt'] = \$nextPosition; - }{$breadcrumbStatements} - - if (!empty(\$thisChanges)) { - \$changes[\$id] = \$thisChanges; - } - - \$nextPosition++; -} - -return \$changes; - - "); - - $this->_addMethod( - $groupByParentsFunctionName, - 'public', - array(sprintf('$%s', $variableNamePlural) => sprintf('array $%s', $variableNamePlural)), - " - -\$grouped = array(); -foreach (\${$variableNamePlural} AS \${$variableName}) { - \$grouped[\${$variableName}['{$parentField}']][\${$variableName}['{$idField}']] = \${$variableName}; -} - -return \$grouped; - - " - ); - } - - protected function _getClassName() - { - return self::getClassName($this->_addOn, $this->_config, $this->_dataClass); - } - - public static function generate(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $g = new self($addOn, $config, $dataClass); - - return array( - $g->_getClassName(), - $g->_generate() - ); - } - - public static function getClassName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return DevHelper_Generator_File::getClassName($addOn['addon_id'], 'Model_' . $dataClass['camelCase'], $config); - } - - public static function getVariableName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $variableName = strtolower(substr($dataClass['camelCase'], 0, 1)) . substr($dataClass['camelCase'], 1); - $variableNamePlural = self::getVariableNamePlural($addOn, $config, $dataClass); - - if ($variableName === $variableNamePlural) { - $variableName = '_' . $variableName; - } - - return $variableName; - } - - public static function getVariableNamePlural(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $variableNamePlural = (empty($dataClass['camelCasePlural']) ? ('All' . $dataClass['camelCase']) : ($dataClass['camelCasePlural'])); - - return strtolower(substr($variableNamePlural, 0, 1)) . substr($variableNamePlural, 1); - } - - public static function generateGetDataFunctionName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return 'get' . (empty($dataClass['camelCasePlural']) ? ('All' . $dataClass['camelCase']) : $dataClass['camelCasePlural']); - } - - public static function generateCountDataFunctionName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return 'count' . (empty($dataClass['camelCasePlural']) ? ('All' . $dataClass['camelCase']) : $dataClass['camelCasePlural']); - } - - public static function generateRebuildStructureFunctionName( - array $addOn, - DevHelper_Config_Base $config, - array $dataClass - ) { - return 'rebuild' . $dataClass['camelCase'] . 'Structure'; - } - - public static function generateGroupByParentsFunctionName( - array $addOn, - DevHelper_Config_Base $config, - array $dataClass - ) { - return 'group' . (empty($dataClass['camelCasePlural']) ? ('All' . $dataClass['camelCase']) : $dataClass['camelCasePlural']) . 'ByParents'; - } - - public static function generateGetPhraseTitleFunctionName( - array $addOn, - DevHelper_Config_Base $config, - array $dataClass, - $phraseType - ) { - $camelCase = ucwords(str_replace('_', ' ', $phraseType)); - return 'getPhraseTitleFor' . $camelCase; - } -} diff --git a/library/DevHelper/Generator/Code/RoutePrefixAdmin.php b/library/DevHelper/Generator/Code/RoutePrefixAdmin.php deleted file mode 100755 index 5590228..0000000 --- a/library/DevHelper/Generator/Code/RoutePrefixAdmin.php +++ /dev/null @@ -1,131 +0,0 @@ -_addOn = $addOn; - $this->_config = $config; - $this->_dataClass = $dataClass; - $this->_info = $info; - } - - protected function _generate() - { - $className = $this->_getClassName(); - - if (count($this->_dataClass['primaryKey']) > 1) { - throw new XenForo_Exception(sprintf('Cannot generate %s: too many fields in primary key', $className)); - } - $idField = reset($this->_dataClass['primaryKey']); - - // create the route prefix first - /** @var XenForo_Model_RoutePrefix $routePrefixModel */ - $routePrefixModel = XenForo_Model::create('XenForo_Model_RoutePrefix'); - $existed = $routePrefixModel->getPrefixesByRouteType('admin'); - foreach ($existed as $routePrefix) { - if ($routePrefix['original_prefix'] == $this->_info['routePrefix'] OR $routePrefix['route_class'] == $className) { - // delete duplicated route prefix - $dw = XenForo_DataWriter::create('XenForo_DataWriter_RoutePrefix'); - $dw->setExistingData($routePrefix); - $dw->delete(); - } - } - - eval("class $className {}"); - $dw = XenForo_DataWriter::create('XenForo_DataWriter_RoutePrefix'); - $dw->bulkSet(array( - 'original_prefix' => $this->_info['routePrefix'], - 'route_type' => 'admin', - 'route_class' => $className, - 'build_link' => 'data_only', - 'addon_id' => $this->_addOn['addon_id'], - )); - $dw->save(); - // finished creating our route prefix - - $this->_setClassName($className); - $this->_addInterface('XenForo_Route_Interface'); - - $this->_addMethod('match', 'public', array( - '$routePath', - '$request' => 'Zend_Controller_Request_Http $request', - '$router' => 'XenForo_Router $router', - ), " - -if (in_array(\$routePath, array('add', 'save'))) { - \$action = \$routePath; -} else { - \$action = \$router->resolveActionWithIntegerParam(\$routePath, \$request, '{$idField}'); -} -return \$router->getRouteMatch('{$this->_info['controller']}', \$action, '{$this->_info['majorSection']}'); - - "); - - $this->_addMethod('buildLink', 'public', array( - '$originalPrefix', - '$outputPrefix', - '$action', - '$extension', - '$data', - '$extraParams' => 'array &$extraParams', - ), " - -if (is_array(\$data)) { - return XenForo_Link::buildBasicLinkWithIntegerParam(\$outputPrefix, \$action, \$extension, \$data, '{$idField}'); -} else { - return XenForo_Link::buildBasicLink(\$outputPrefix, \$action, \$extension); -} - - "); - - return parent::_generate(); - } - - protected function _getClassName() - { - return self::getClassName($this->_addOn, $this->_config, $this->_dataClass); - } - - public static function generate(array $addOn, DevHelper_Config_Base $config, array $dataClass, array $info) - { - $g = new self($addOn, $config, $dataClass, $info); - - return array( - $g->_getClassName(), - $g->_generate() - ); - } - - public static function getClassName(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - return DevHelper_Generator_File::getClassName( - $addOn['addon_id'], - 'Route_PrefixAdmin_' . $dataClass['camelCase'], - $config - ); - } - - public static function getRoutePrefix(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $className = self::getClassName($addOn, $config, $dataClass); - - /** @var XenForo_Model_RoutePrefix $routePrefixModel */ - $routePrefixModel = XenForo_Model::create('XenForo_Model_RoutePrefix'); - $routePrefixes = $routePrefixModel->getPrefixesByAddOnGroupedByRouteType($addOn['addon_id']); - if (!empty($routePrefixes['admin'])) { - foreach ($routePrefixes['admin'] as $routePrefix) { - if ($routePrefix['route_class'] == $className) { - return $routePrefix['original_prefix']; - } - } - } - - return strtolower($addOn['addon_id'] . '-' . $dataClass['name']); - } -} diff --git a/library/DevHelper/Generator/Code/XenForoConfig.php b/library/DevHelper/Generator/Code/XenForoConfig.php deleted file mode 100644 index 16d445f..0000000 --- a/library/DevHelper/Generator/Code/XenForoConfig.php +++ /dev/null @@ -1,55 +0,0 @@ -getRootDir() . '/library/config.php'; - $originalContents = file_get_contents($path); - - $keyParts = explode('.', $key); - $varNamePattern = '#(\n|^)(\\$config'; - foreach ($keyParts as $i => $keyPart) { - // try to match the quote - $varNamePattern .= '\\[([\'"]?)' - // then the key - . preg_quote($keyPart, '#') - // then match the previously matched quote - . '\\' . ($i + 3) . '\\]'; - } - $varNamePattern .= ').+(\n|$)#'; - - $candidates = array(); - $offset = 0; - while (true) { - if (!preg_match($varNamePattern, $originalContents, $matches, PREG_OFFSET_CAPTURE, $offset)) { - break; - } - - $offset = $matches[0][1] + strlen($matches[0][0]); - $candidates[] = $matches; - } - - if (count($candidates) > 1) { - throw new XenForo_Exception(sprintf('count($candidates) = %d', count($candidates))); - } - - $phpStatement = sprintf( - '$config["%s"] = %s;', - implode('"]["', $keyParts), - var_export($value, true) - ); - if (count($candidates) === 1) { - $matches = reset($candidates); - - $replacement = $matches[1][0] . $phpStatement . $matches[5][0]; - $contents = substr_replace($originalContents, $replacement, $matches[0][1], strlen($matches[0][0])); - } else { - $contents = $originalContents . "\n\n" . $phpStatement; - } - - DevHelper_Generator_File::writeFile($path, $contents, true, false); - } -} diff --git a/library/DevHelper/Generator/Db.php b/library/DevHelper/Generator/Db.php deleted file mode 100755 index cad8d7e..0000000 --- a/library/DevHelper/Generator/Db.php +++ /dev/null @@ -1,360 +0,0 @@ -getDataPatches() as $patchTableName => $patchTablePatches) { - if ($patchTableName !== $tableName) { - continue; - } - - foreach ($patchTablePatches as $dataPatch) { - if (!empty($dataPatch['index'])) { - $indexConfigs[$dataPatch['name']] = $dataPatch; - } else { - $fieldConfigs[$dataPatch['name']] = $dataPatch; - } - } - } - - $fields = array(); - foreach ($fieldConfigs as $field) { - $fields[] = "`$field[name]` " . self::_getFieldDefinition($field); - } - $fields = implode("\n ,", $fields); - - if (!empty($dataClass['primaryKey'])) { - $primaryKey = ", PRIMARY KEY (`" . implode('`,`', $dataClass['primaryKey']) . "`)"; - } else { - $primaryKey = ''; - } - - $indeces = array(); - foreach ($indexConfigs as $index) { - $indeces[] = self::_getIndexDefinition($index); - } - $indeces = implode("\n ,", $indeces); - if (!empty($indeces)) { - $indeces = ',' . $indeces; - } - - $sql = <<getPrefix()) !== false - ) { - return $name; - } else { - return 'xf_' . self::getFieldName($config, $name, true); - } - } - - public static function getFieldName(DevHelper_Config_Base $config, $name, $ignoreDash = false) - { - if ($ignoreDash OR strpos($name, '_') === false) { - return strtolower($config->getPrefix() . '_' . $name); - } else { - return strtolower($name); - } - } - - public static function getConditionFields(array $fields) - { - $conditionsFields = array(); - - $intTypes = array( - XenForo_DataWriter::TYPE_BOOLEAN, - XenForo_DataWriter::TYPE_INT, - XenForo_DataWriter::TYPE_UINT, - XenForo_DataWriter::TYPE_UINT_FORCED, - ); - $imageFields = self::getImageFields($fields); - - foreach ($fields as $field) { - if (in_array($field['name'], $imageFields)) { - continue; - } - - if (in_array($field['type'], $intTypes)) { - $conditionsFields[] = $field['name']; - continue; - } - - if ($field['type'] == 'string' AND isset($field['length']) AND $field['length'] <= 255) { - // this is a VARCHAR one - $conditionsFields[] = $field['name']; - continue; - } - } - - return $conditionsFields; - } - - public static function getImageFields(array $fields) - { - $imageFields = array(); - - foreach ($fields as $field) { - if (substr($field['name'], -10) == 'image_date') { - $imageFields[] = $field['name']; - } - } - - return $imageFields; - } - - public static function getImageField(array $fields) - { - $imageFields = self::getImageFields($fields); - - if (count($imageFields) == 1) { - // only return the image field if there is 1 image field - // if there is no image fields or more than 1, simply ignore them all - return $imageFields[0]; - } else { - return false; - } - } - - public static function getOptionsFields(array $fields) - { - $optionsFields = array(); - - foreach ($fields as $field) { - if (substr($field['name'], -8) == '_options' AND $field['type'] == XenForo_DataWriter::TYPE_SERIALIZED) { - $optionsFields[] = $field['name']; - } - } - - return $optionsFields; - } - - public static function getParentField($className, array $fields) - { - $parentFieldNames = array( - sprintf('%s_parent_id', $className), - sprintf('parent_%s_id', $className), - 'parent_id', - ); - - foreach ($fields as $field) { - if (in_array($field['name'], $parentFieldNames)) { - return $field['name']; - } - } - - return false; - } - - public static function getBreadcrumbField($className, array $fields) - { - $breadcrumbFieldNames = array( - sprintf('%s_breadcrumb', $className), - 'breadcrumb', - ); - - foreach ($fields as $field) { - if (in_array($field['name'], $breadcrumbFieldNames)) { - return $field['name']; - } - } - - return false; - } - - public static function getDataTypes() - { - return $types = array( - XenForo_DataWriter::TYPE_BOOLEAN, - XenForo_DataWriter::TYPE_STRING, - XenForo_DataWriter::TYPE_BINARY, - XenForo_DataWriter::TYPE_INT, - XenForo_DataWriter::TYPE_UINT, - XenForo_DataWriter::TYPE_UINT_FORCED, - XenForo_DataWriter::TYPE_FLOAT, - XenForo_DataWriter::TYPE_SERIALIZED, - ); - } - - protected static function _getFieldDefinition($field) - { - switch ($field['type']) { - case XenForo_DataWriter::TYPE_BOOLEAN: - $dbType = 'TINYINT(4) UNSIGNED'; - break; - case XenForo_DataWriter::TYPE_STRING: - if (!isset($field['length']) || $field['length'] > 255) { - $dbType = 'TEXT'; - if (isset($field['length'])) { - if ($field['length'] >= 4294967295) { - $dbType = 'LONGTEXT'; - } elseif ($field['length'] >= 16777215) { - $dbType = 'MEDIUMTEXT'; - } - } - if (isset($field['default'])) { - // BLOB/TEXT column can't have a default value - unset($field['default']); - } - } else { - if (!empty($field['allowedValues'])) { - // ENUM - $dbType = 'ENUM (\'' . implode('\',\'', $field['allowedValues']) . '\')'; - } else { - $dbType = 'VARCHAR(' . $field['length'] . ')'; - } - } - break; - case XenForo_DataWriter::TYPE_BINARY: - if (!isset($field['length']) || $field['length'] > 255) { - $dbType = 'BLOB'; - if (isset($field['length'])) { - if ($field['length'] >= 4294967295) { - $dbType = 'LONGBLOB'; - } elseif ($field['length'] >= 16777215) { - $dbType = 'MEDIUMBLOB'; - } - } - } else { - $dbType = 'VARBINARY(' . $field['length'] . ')'; - } - if (isset($field['default'])) { - // BLOB/TEXT column can't have a default value - unset($field['default']); - } - break; - case XenForo_DataWriter::TYPE_INT: - $dbType = 'INT(11)'; - if (isset($field['length'])) { - if ($field['length'] == 4) { - $dbType = 'TINYINT(' . $field['length'] . ')'; - } - } - break; - case XenForo_DataWriter::TYPE_UINT: - case XenForo_DataWriter::TYPE_UINT_FORCED: - $dbType = 'INT(10) UNSIGNED'; - break; - case XenForo_DataWriter::TYPE_FLOAT: - $dbType = 'FLOAT'; - break; - case 'money': - $dbType = 'DECIMAL(13,4)'; - break; - case XenForo_DataWriter::TYPE_SERIALIZED: - default: - $dbType = 'MEDIUMBLOB'; - if (isset($field['default'])) { - unset($field['default']); - } - // BLOB/TEXT column can't have a default value - break; - } - - return $dbType . (!empty($field['required']) ? ' NOT NULL' : '') . - (isset($field['default']) ? " DEFAULT '{$field['default']}'" : '') . - (!empty($field['autoIncrement']) ? ' AUTO_INCREMENT' : ''); - } - - public static function getIndexTypes() - { - return array( - 'NORMAL', - 'UNIQUE', - 'FULLTEXT', - 'SPATIAL', - ); - } - - protected static function _getIndexDefinition($index) - { - $indexName = $index['name']; - $indexType = strtoupper($index['type']); - - $definition = ($indexType != 'NORMAL' ? ($index['type'] . ' ') : '') - . "INDEX `{$indexName}` (`" . implode('`,`', $index['fields']) . "`)"; - - return $definition; - } -} diff --git a/library/DevHelper/Generator/File.php b/library/DevHelper/Generator/File.php deleted file mode 100755 index 2e5b757..0000000 --- a/library/DevHelper/Generator/File.php +++ /dev/null @@ -1,747 +0,0 @@ -get(sprintf('DevHelper_%sConfigClass', $addOnId)); - if ($addOnId === 'devHelper') { - $className = 'DevHelper_DevHelper_Config'; - } - if (empty($className)) { - $className = sprintf('%s_DevHelper_Config', $addOnId); - } - } else { - if ($config === null) { - throw new XenForo_Exception(sprintf( - '%s requires $config when $subClassName=%s', - __METHOD__, - $subClassName - )); - } - - $className = rtrim(sprintf('%s_%s', $config->getClassPrefix(), $subClassName), '_'); - } - - $classNames[$hash] = $className; - } - - return $classNames[$hash]; - } - - public static function getClassPath($className, DevHelper_Config_Base $config = null) - { - if ($config === null) { - return DevHelper_Autoloader::getDevHelperInstance()->autoloaderClassToFile($className); - } else { - $configClass = get_class($config); - $thisClassParts = explode('_', $className); - $configClassParts = explode('_', $configClass); - - $configClassPath = self::getClassPath($configClass); - $configClassPathParts = explode('/', $configClassPath); - while ($thisClassParts[0] === $configClassParts[0]) { - array_shift($thisClassParts); - array_shift($configClassParts); - } - while (count($configClassParts) > 0) { - array_pop($configClassParts); - array_pop($configClassPathParts); - } - - $path = sprintf('%s/%s.php', implode('/', $configClassPathParts), implode('/', $thisClassParts)); - - return $path; - } - } - - public static function getAddOnIdPath(DevHelper_Config_Base $config) - { - $libraryPath = self::getLibraryPath($config); - $path = $libraryPath . DIRECTORY_SEPARATOR . - str_replace('_', DIRECTORY_SEPARATOR, $config->getClassPrefix()); - - return $path; - } - - public static function getLibraryPath(DevHelper_Config_Base $config) - { - $configClassPath = self::getClassPath(get_class($config)); - $path = $configClassPath; - - do { - if (empty($path)) { - throw new XenForo_Exception(sprintf('Cannot find library path for %s', get_class($config))); - } - $path = dirname($path); - } while (basename($path) !== 'library'); - - return $path; - } - - public static function getAddOnXmlPath( - array $addOn, - array $exportAddOn = null, - DevHelper_Config_Base $config = null - ) { - if ($config === null) { - /** @var DevHelper_Model_Config $configModel */ - $configModel = XenForo_Model::create('DevHelper_Model_Config'); - $config = $configModel->loadAddOnConfig($addOn); - } - - $addOnIdPath = self::getAddOnIdPath($config); - $addOnId = (!empty($exportAddOn) ? $exportAddOn['addon_id'] : $addOn['addon_id']); - - return $addOnIdPath . '/addon-' . $addOnId . '.xml'; - } - - public static function getStyleXmlPath( - array $addOn, - array $style, - DevHelper_Config_Base $config - ) { - $addOnIdPath = self::getAddOnIdPath($config); - - return $addOnIdPath . '/style-' . $style['title'] . '.xml'; - } - - public static function writeFile($path, $contents, $backUp, $isAutoGenerated) - { - $skip = false; - - if (file_exists($path)) { - // existed file - $oldContents = self::fileGetContents($path); - - if ($oldContents == $contents) { - // same content - $skip = true; - } else { - if ($backUp) { - if (strpos($path, 'FileSums.php') !== false) { - // writing FileSums.php - // this file is generated so many times that it's annoying - // so we will skip saving a copy of it... - } else { - copy($path, $path . '.' . XenForo_Application::$time . '.devhelper'); - } - } - - if ($isAutoGenerated AND XenForo_Helper_File::getFileExtension($path) === 'php') { - // different php content - // try to replace the auto generated code only - $startPosOld = strpos($oldContents, self::COMMENT_AUTO_GENERATED_START); - $endPosOld = strpos($oldContents, self::COMMENT_AUTO_GENERATED_END, $startPosOld); - - if ($startPosOld !== false AND $endPosOld !== false AND $endPosOld > $startPosOld) { - // found our comments in old contents - $startPos = strpos($contents, self::COMMENT_AUTO_GENERATED_START); - $endPos = strpos($contents, self::COMMENT_AUTO_GENERATED_END, $startPos); - - if ($startPos !== false AND $endPos !== false AND $endPos > $startPos) { - // found our comments in new contents - $replacement = substr($contents, $startPos, $endPos - $startPos); - $start = $startPosOld; - $length = $endPosOld - $startPosOld; - - $contents = substr_replace($oldContents, $replacement, $start, $length); - } - } - } - } - } - - if (!$skip) { - return self::filePutContents($path, $contents); - } else { - return 'skip'; - } - } - - public static function writeClass($className, $contents, DevHelper_Config_Base $config = null) - { - $path = self::getClassPath($className, $config); - - self::writeFile($path, $contents, true, true); - - if (strpos($className, 'DevHelper_Generated') === false) { - $backupClassName = self::_getBackupClassName($className); - $backupPath = self::getClassPath($backupClassName, $config); - self::writeFile($backupPath . '.devhelper', $contents, false, false); - } - - return $path; - } - - protected static function _getBackupClassName($className) - { - $parts = explode('_', $className); - $prefix = array_shift($parts); - $suffix = implode('_', $parts); - return $prefix . '_DevHelper_Generated_' . $suffix; - } - - public static function fileGetContents($path) - { - if (is_readable($path)) { - $contents = file_get_contents($path); - - return $contents; - } else { - return false; - } - } - - public static function filePutContents($path, $contents) - { - $dir = dirname($path); - XenForo_Helper_File::createDirectory($dir); - if (!is_dir($dir) OR !is_writable($dir)) { - return false; - } - - if (file_put_contents($path, $contents) > 0) { - XenForo_Helper_File::makeWritableByFtpUser($path); - return true; - } - - return false; - } - - public static function generateHashesFile( - array $addOn, - DevHelper_Config_Base $config, - array $directories, - $exportPath, - $rootPath - ) { - $hashes = array(); - $exportPath = rtrim($exportPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - $rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - - $excludes = $config->getExportExcludes(); - - foreach ($directories as $key => $directory) { - $directory = preg_replace('#^' . preg_quote($rootPath, '#') . '#', $exportPath, $directory); - $directoryHashes = XenForo_Helper_Hash::hashDirectory($directory, array( - '.php', - '.js' - )); - - foreach ($directoryHashes as $filePath => $hash) { - if (strpos($filePath, 'DevHelper') === false AND strpos($filePath, 'FileSums') === false) { - $relative = preg_replace('#^' . preg_quote($exportPath, '#') . '#', '', $filePath); - - $excluded = false; - foreach ($excludes as $exclude) { - if (strpos($relative, $exclude) === 0) { - $excluded = true; - break; - } - } - if ($excluded) { - continue; - } - - $hashes[$relative] = $hash; - } - } - } - - $fileSumsClassName = self::getClassName($addOn['addon_id'], 'FileSums', $config); - $hashesExported = self::varExport($hashes, 2); - $fileSumsContents = "getClassPrefix(); - - $libraryPath = self::getLibraryPath($config); - $rootPath = dirname($libraryPath); - - $list['library'] = self::getAddOnIdPath($config); - if (empty($list['library'])) { - throw new XenForo_Exception(sprintf('`library` not found for %s', $addOn['addon_id'])); - } - - $jsPath = sprintf('%s/js/%s', $rootPath, str_replace('_', DIRECTORY_SEPARATOR, $classPrefix)); - if (is_dir($jsPath)) { - $list['js'] = $jsPath; - } - - $stylesDefaultPath = sprintf( - '%s/styles/default/%s', - $rootPath, - str_replace('_', DIRECTORY_SEPARATOR, $classPrefix) - ); - if (is_dir($stylesDefaultPath)) { - $list['styles_default'] = $stylesDefaultPath; - } - - $exportIncludes = $config->getExportIncludes(); - foreach ($exportIncludes as $exportInclude) { - $exportIncludePath = sprintf('%s/%s', $rootPath, $exportInclude); - - if (file_exists($exportIncludePath)) { - $list[$exportInclude] = $exportIncludePath; - } - } - - // save add-on XML - $xmlPath = self::getAddOnXmlPath($addOn, null, $config); - - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = XenForo_Model::create('XenForo_Model_AddOn'); - $addOnModel->getAddOnXml($addOn)->save($xmlPath); - echo "Exported $xmlPath ($addOn[version_string]/$addOn[version_id])\n"; - DevHelper_Helper_Phrase::parseXmlForPhraseTracking($xmlPath); - - $exportAddOns = $config->getExportAddOns(); - foreach ($exportAddOns as $exportAddOnId) { - $exportAddOn = $addOnModel->getAddOnById($exportAddOnId); - if (empty($exportAddOn)) { - die(sprintf("Could not find add-on %s\n", $exportAddOnId)); - } - - $exportAddOnPath = self::getAddOnXmlPath($addOn, $exportAddOn, $config); - $addOnModel->getAddOnXml($exportAddOn)->save($exportAddOnPath); - echo "Exported $exportAddOnPath ($exportAddOn[version_string]/$exportAddOn[version_id])\n"; - } - - $exportStyles = $config->getExportStyles(); - if (!empty($exportStyles)) { - /** @var XenForo_Model_Style $styleModel */ - $styleModel = $addOnModel->getModelFromCache('XenForo_Model_Style'); - $styles = $styleModel->getAllStyles(); - $exportedStyleCount = 0; - - foreach ($styles as $style) { - if (in_array($style['title'], $exportStyles, true)) { - $stylePath = self::getStyleXmlPath($addOn, $style, $config); - $styleModel->getStyleXml($style)->save($stylePath); - echo "Exported $stylePath\n"; - $exportedStyleCount++; - } - } - - if ($exportedStyleCount < count($exportStyles)) { - die("Not all export styles could be found...\n"); - } - } - - // check for file_health_check event listener - /** @var XenForo_Model_CodeEvent $codeEventModel */ - $codeEventModel = XenForo_Model::create('XenForo_Model_CodeEvent'); - $addOnEventListeners = $codeEventModel->getEventListenersByAddOn($addOn['addon_id']); - $fileHealthCheckFound = false; - foreach ($addOnEventListeners as $addOnEventListener) { - if ($addOnEventListener['event_id'] === 'file_health_check') { - $fileHealthCheckFound = true; - } - - if (!is_callable(array($addOnEventListener['callback_class'], $addOnEventListener['callback_method']))) { - die(sprintf( - "Callback is not callable %s::%s\n", - $addOnEventListener['callback_class'], - $addOnEventListener['callback_method'] - )); - } - } - if (!$fileHealthCheckFound) { - // try to generate the file health check event listener ourselves - if (DevHelper_Generator_Code_Listener::generateFileHealthCheck($addOn, $config)) { - $fileHealthCheckFound = true; - } - } - if (!$fileHealthCheckFound) { - die("No `file_health_check` event listener found.\n"); - } - - if (strpos($exportPath, 'upload') === false) { - $exportPath .= '/upload'; - } - XenForo_Helper_File::createDirectory($exportPath /*, true */); - $exportPath = realpath($exportPath); - $options = array( - 'extensions' => array( - 'php', - 'inc', - 'txt', - 'xml', - 'htm', - 'html', - 'js', - 'css', - 'jpg', - 'jpeg', - 'png', - 'gif', - 'swf', - 'crt', - 'pem', - 'eot', - 'svg', - 'ttf', - 'woff', - 'woff2', - 'otf', - 'md', - ), - 'filenames_lowercase' => array( - 'license', - 'readme', - 'copyright', - '.htaccess', - 'changelog', - 'composer.json', - 'readme.rdoc', - 'version', - ), - 'force' => true, // always force add top level export entries - 'addon_id' => $addOn['addon_id'], - - 'excludes' => array(), - 'excludeRegExs' => array(), - ); - - $excludes = $config->getExportExcludes(); - foreach ($excludes as $exclude) { - if (preg_match('/^#.+#$/', $exclude)) { - $options['excludeRegExs'][] = $exclude; - } else { - $options['excludes'][] = $exclude; - } - } - - // run phpcbf by default if available - // append phpcs=1 in file-export url to run phpcs instead - if (!empty($_REQUEST['phpcbf'])) { - $codingStandardBinary = exec('which phpcbf'); - } else { - $codingStandardBinary = exec('which phpcs'); - if (!empty($codingStandardBinary)) { - $codingStandardBinary .= ' -s'; - } - } - $phpcsOutput = array(); - foreach ($list as $type => $entry) { - if (!empty($codingStandardBinary)) { - exec(sprintf( - '%1$s --standard=%3$s %2$s', - $codingStandardBinary, - escapeshellarg($entry), - escapeshellarg(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'phpcs.xml') - ), $phpcsOutput); - } - - self::_fileExport($entry, $exportPath, $rootPath, array_merge($options, array('type' => $type))); - } - - $hashesFilePath = self::generateHashesFile($addOn, $config, $list, $exportPath, $rootPath); - self::_fileExport($hashesFilePath, $exportPath, $rootPath, $options); - - // copy one xml copy to the export directory directory - $xmlCopyPath = sprintf('%s/%s', dirname($exportPath), basename($xmlPath)); - if (@copy($xmlPath, $xmlCopyPath)) { - echo "Copied $xmlPath -> $xmlCopyPath\n"; - } else { - echo "Can't cp $xmlPath -> $xmlCopyPath\n"; - } - - echo(implode("\n", $phpcsOutput)); - } - - protected static function _fileExport($entry, &$exportPath, &$rootPath, $options) - { - if (empty($entry)) { - return; - } - - $relativePath = trim(str_replace($rootPath, '', $entry), '/'); - - if (empty($options['force'])) { - if (in_array($relativePath, $options['excludes'])) { - echo "Excluded $relativePath\n"; - return; - } - - foreach ($options['excludeRegExs'] as $excludeRegEx) { - if (preg_match($excludeRegEx, $relativePath)) { - echo "RegExcluded $relativePath\n"; - return; - } - } - - $baseName = basename($relativePath); - switch ($baseName) { - case 'FileSums.php': - case 'upload': - return; - } - } - - if (is_dir($entry)) { - echo "Browsing $relativePath\n"; - - $children = array(); - - $dh = opendir($entry); - while ($child = readdir($dh)) { - if (substr($child, 0, 1) === '.') { - // ignore . (current directory) - // ignore .. (parent directory) - // ignore hidden files (dot files/directories) - continue; - } - - $children[] = $child; - } - - foreach ($children as $child) { - if (!empty($options['force'])) { - $options['force'] = false; - } - // reset `force` option for children - self::_fileExport($entry . '/' . $child, $exportPath, $rootPath, $options); - } - } elseif (is_file($entry)) { - $ext = XenForo_Helper_File::getFileExtension($entry); - if (!empty($options['force']) - || (in_array($ext, $options['extensions']) - && strpos(basename($entry), '.') !== 0) - || in_array(strtolower(basename($entry)), $options['filenames_lowercase']) - ) { - if ($options['addon_id'] == 'devHelper') { - $isDevHelper = (strpos($entry, 'DevHelper/DevHelper') !== false); - } else { - $isDevHelper = (strpos($entry, 'DevHelper') !== false); - } - - if (!$isDevHelper) { - $entryExportPath = $exportPath . '/' . $relativePath; - $entryExportPathOriginal = $entryExportPath; - $contents = null; - - switch ($ext) { - case 'js': - if ($options['type'] === 'js' - && preg_match('#\.min\.js$#', $entry) - ) { - $fullEntry = sprintf( - '%s/full/%s', - dirname($entry), - preg_replace('#\.min\.js$#', '.js', basename($entry)) - ); - if (self::fileGetContents($fullEntry)) { - $contents = ''; - } - } - break; - case 'php': - DevHelper_Helper_ShippableHelper::checkForUpdate($entry); - break; - } - - if ($contents === null) { - $contents = self::fileGetContents($entry); - } - - if (!empty($contents)) { - switch ($ext) { - case 'js': - if ($options['type'] === 'js' - && basename(dirname($entry)) === 'full' - && !preg_match('#\.min\.js$#', $entry) - ) { - $entryExportPath = sprintf( - '%s/%s', - dirname(dirname($entryExportPathOriginal)), - preg_replace('#\.js$#', '.min.js', basename($entry)) - ); - $minPath = DevHelper_Helper_Js::minifyPath($entry); - $contents = self::fileGetContents($minPath); - } - break; - case 'php': - DevHelper_Helper_Phrase::parsePhpForPhraseTracking($relativePath, $contents); - DevHelper_Helper_Xfcp::parsePhpForXfcpClass($relativePath, $contents); - break; - } - - $result = self::writeFile($entryExportPath, $contents, false, false); - } else { - $result = 'empty'; - } - - if ($result === true) { - $result = 'OK'; - if ($entryExportPath !== $entryExportPathOriginal) { - $result = preg_replace( - '#^' . preg_quote($exportPath, '#') . '/#', - '', - $entryExportPath - ); - $result = '-> ' . $result; - } - - echo "Exporting {$relativePath} $result\n"; - } else { - echo "Exporting {$relativePath} $result\n"; - } - } - } - } - } - - public static function varExport($var, $level = 1, $linePrefix = " ", $noKey = false) - { - $output = ''; - - if (is_array($var)) { - $arrayVars = array(); - $multiLine = false; - $keyValueLength = 0; - $allKeysAreInt = true; - foreach ($var as $key => $value) { - $arrayVars[$key] = self::varExport($value, $level + 1, $linePrefix); - if (is_array($value) AND count($value) > 1) { - $multiLine = true; - } - if (strpos($arrayVars[$key], "\n") !== false) { - $multiLine = true; - } - - $keyValueLength += strlen($key); - if (is_array($value)) { - $keyValueLength += strlen(var_export($value, true)); - } else { - $keyValueLength += strlen($value); - } - - if (!is_int($key)) { - $allKeysAreInt = false; - } - } - if ($keyValueLength > 100) { - $multiLine = true; - } - if ($allKeysAreInt) { - $noKey = true; - } - - $output .= 'array('; - $first = true; - foreach ($arrayVars as $key => $str) { - if ($multiLine) { - $output .= "\n" . str_repeat($linePrefix, $level + 1); - } else { - if (!$first) { - $output .= ', '; - } - } - - if (empty($noKey)) { - $output .= var_export($key, true) . ' => '; - } - - $output .= $str; - - if ($multiLine) { - $output .= ','; - } - - $first = false; - } - - if ($multiLine) { - $output .= "\n" . str_repeat($linePrefix, $level); - } - - $output .= ')'; - } elseif (is_object($var) && $var instanceof _DevHelper_Generator_File_Constant) { - $output .= strval($var); - } else { - $tmp = var_export($var, true); - if (strpos($tmp, "\n") !== false) { - $tmp = str_replace("\n", "\n" . str_repeat($linePrefix, $level), $tmp); - } - - $output .= $tmp; - } - - return $output; - } - - public static function varExportConstant($str) - { - return new _DevHelper_Generator_File_Constant($str); - } - - public static function varExportConstantFromArray($value, array $constants) - { - foreach ($constants as $constant) { - if ($value === constant($constant)) { - return self::varExportConstant($constant); - } - } - - return $value; - } -} - -class _DevHelper_Generator_File_Constant -{ - protected $_str = ''; - - public function __construct($str) - { - $this->_str = $str; - } - - public function __toString() - { - return $this->_str; - } -} diff --git a/library/DevHelper/Generator/Phrase.php b/library/DevHelper/Generator/Phrase.php deleted file mode 100755 index a9e6ff4..0000000 --- a/library/DevHelper/Generator/Phrase.php +++ /dev/null @@ -1,64 +0,0 @@ -bulkSet(array( - 'title' => $title, - 'phrase_text' => $phraseText, - 'language_id' => 0, - 'global_cache' => 0, - 'addon_id' => $addOn['addon_id'], - )); - $writer->updateVersionId(); - $writer->save(); - - return true; - } - - public static function generatePhraseAutoCamelCaseStyle( - array $addOn, - DevHelper_Config_Base $config, - array $dataClass, - $dashText - ) { - $camelCase = ucwords(str_replace('_', ' ', $dashText)); - $title = self::getPhraseName($addOn, $config, $dataClass, $dashText); - self::generatePhrase($addOn, $title, $camelCase); - - return $title; - } - - public static function checkPhraseExists($title) - { - $info = self::_getPhraseModel()->getPhrasesInLanguageByTitles(array($title), 0); - - return !empty($info); - } - - /** - * @return XenForo_Model_Phrase - */ - protected static function _getPhraseModel() - { - /** @var XenForo_Model_Phrase $model */ - static $model = null; - - if ($model === null) { - $model = XenForo_Model::create('XenForo_Model_Phrase'); - } - - return $model; - } -} diff --git a/library/DevHelper/Generator/Template.php b/library/DevHelper/Generator/Template.php deleted file mode 100755 index 0a512bc..0000000 --- a/library/DevHelper/Generator/Template.php +++ /dev/null @@ -1,70 +0,0 @@ -keyPropertiesByName($propertyModel->getEffectiveStylePropertiesInStyle(-1)); - $propertyChanges = $propertyModel->translateEditorPropertiesToArray($template, $template, $properties); - - $writer = XenForo_DataWriter::create('XenForo_DataWriter_AdminTemplate'); - $writer->bulkSet(array( - 'title' => $title, - 'template' => $template, - 'addon_id' => $addOn['addon_id'], - )); - - try { - $writer->save(); - } catch (Exception $ex) { - throw new XenForo_Exception("Exception creating template $title: " . - $ex->getMessage() . '
' . htmlentities($template) . '
'); - } - - $propertyModel->saveStylePropertiesInStyleFromTemplate(-1, $propertyChanges, $properties); - - return true; - } - - public static function checkAdminTemplateExists($title) - { - $info = self::_getAdminTemplateModel()->getAdminTemplateByTitle($title); - - return !empty($info); - } - - /** - * @return XenForo_Model_AdminTemplate - */ - protected static function _getAdminTemplateModel() - { - /** @var XenForo_Model_AdminTemplate $model */ - static $model = null; - - if ($model === null) { - $model = XenForo_Model::create('XenForo_Model_AdminTemplate'); - } - - return $model; - } - - /** - * @return XenForo_Model_StyleProperty - */ - protected static function _getStylePropertyModel() - { - /** @noinspection PhpIncompatibleReturnTypeInspection */ - return self::_getAdminTemplateModel()->getModelFromCache('XenForo_Model_StyleProperty'); - } -} diff --git a/library/DevHelper/Generator/phpcs.xml b/library/DevHelper/Generator/phpcs.xml deleted file mode 100644 index 23c95fb..0000000 --- a/library/DevHelper/Generator/phpcs.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - */CodeMirror/ - */DevHelper/Generated/ - */Lib/ - */ShippableHelper/ - */tests/ - *.min.js - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/library/DevHelper/Helper/File.php b/library/DevHelper/Helper/File.php deleted file mode 100644 index 6fdb444..0000000 --- a/library/DevHelper/Helper/File.php +++ /dev/null @@ -1,34 +0,0 @@ - static::FIND_XML_IF_PATHS_COUNT_LESS_THAN) { - return ''; - } - - $subPaths = array(); - foreach ($paths as $path) { - if (!is_dir($path)) { - continue; - } - - $contentPaths = glob(sprintf('%s/*', rtrim($path, '/'))); - foreach ($contentPaths as $contentPath) { - if (is_dir($contentPath)) { - $subPaths[] = $contentPath . '/'; - } else { - $ext = XenForo_Helper_File::getFileExtension($contentPath); - if ($ext === 'xml') { - return $contentPath; - } - } - } - } - - return static::findXml($subPaths); - } -} diff --git a/library/DevHelper/Helper/Js.php b/library/DevHelper/Helper/Js.php deleted file mode 100644 index 4d985f0..0000000 --- a/library/DevHelper/Helper/Js.php +++ /dev/null @@ -1,83 +0,0 @@ -&1', - escapeshellarg($fullPath), - escapeshellarg($minPath), - escapeshellarg(sprintf( - 'root=full,base="%1$s",url=%2$s.map', - dirname($fullPath), - basename($minPath) - )) - ); - - exec($command, $uglifyOutput, $uglifyResult); - - if (!file_exists($minPath) || $uglifyResult !== 0) { - echo(sprintf("%s: %s\n%s -> %d
\n", __METHOD__, $fullPath, $command, $uglifyResult)); - var_export($uglifyOutput); - exit($uglifyResult); - } - - return $minPath; - } - - public static function processJsFiles(array $jsFiles) - { - foreach ($jsFiles as $path) { - if (strpos($path, 'js') !== 0) { - // ignore non js files - continue; - } - - if (strpos($path, 'js/xenforo/') === 0) { - // ignore xenforo files - continue; - } - - if (!preg_match('#\.min\.js$#', $path)) { - // ignore non .min.js files - continue; - } - - /** @var XenForo_Application $application */ - $application = XenForo_Application::getInstance(); - $fullPath = sprintf( - '%s/%s/full/%s', - $application->getRootDir(), - dirname($path), - preg_replace('#\.min\.js$#', '.js', basename($path)) - ); - if (!empty($_SERVER['DEVHELPER_ROUTER_PHP'])) { - $fullPath = DevHelper_Router::locate($fullPath); - } - - if (file_exists($fullPath)) { - static::minifyPath($fullPath); - } - } - } -} diff --git a/library/DevHelper/Helper/Php.php b/library/DevHelper/Helper/Php.php deleted file mode 100644 index bd2b40e..0000000 --- a/library/DevHelper/Helper/Php.php +++ /dev/null @@ -1,107 +0,0 @@ -= $phpLength) { - // unclosed string - return false; - } - $next = substr($php, $innerOffset, 1); - } elseif ($next === $operator) { - // found another instance of the operator, end of string - $offset = $innerOffset; - return $string; - } - - $string .= $next; - $innerOffset++; - if ($innerOffset >= $phpLength) { - // unclosed string - return false; - } - } - break; - } - - return false; - } - - public static function extractMethods($php) - { - $methods = array(); - - $offset = 0; - while (true) { - if (preg_match( - '#(public|protected|private|static|\s)*' - . 'function\s+(?[a-zA-Z0-9_]+)\s*\([^\)]*\)\s*{#', - $php, - $matches, - PREG_OFFSET_CAPTURE, - $offset - )) { - $methods[] = $matches['method'][0]; - $offset = $matches[0][1] + strlen($matches[0][0]); - } else { - break; - } - } - - return $methods; - } - - public static function appendMethod($php, $methodCode) - { - $lines = explode("\n", $php); - $backUp = array(); - - while (true) { - $lastLine = array_pop($lines); - $pos = strrpos($lastLine, '}'); - if ($pos === false) { - $backUp[] = $lastLine; - continue; - } - - $prev = substr($lastLine, 0, $pos); - $lines[] = $prev; - - $indent = ' '; - if (preg_match('#^(?\s+)([^\s]|$)#', $prev, $matches)) { - $indent .= $matches['indent']; - } - - $methodCodeLines = explode("\n", $methodCode); - foreach ($methodCodeLines as $methodCodeLine) { - $lines[] = $indent . $methodCodeLine; - } - - $lines[] = substr($lastLine, $pos); - break; - } - - $contents = implode("\n", $lines); - if (!empty($backUp)) { - $contents .= "\n" . implode("\n", array_reverse($backUp)); - } - return $contents; - } -} diff --git a/library/DevHelper/Helper/Phrase.php b/library/DevHelper/Helper/Phrase.php deleted file mode 100644 index d131ba4..0000000 --- a/library/DevHelper/Helper/Phrase.php +++ /dev/null @@ -1,149 +0,0 @@ -{$phrase['title']}\n"; - } else { - $usedPhraseTitles[] = $phrase['title']; - } - } - - foreach ($foundPhraseTitles as $phraseTitle) { - if (!in_array($phraseTitle, $usedPhraseTitles)) { - $foundButUnusedPhraseTitles[] = $phraseTitle; - } - } - if (!empty($foundButUnusedPhraseTitles)) { - $foundButUnusedPhrases = $phraseModel->getPhrasesInLanguageByTitles($foundButUnusedPhraseTitles); - foreach ($foundButUnusedPhraseTitles as $phraseTitle) { - $good = true; - - if (empty($foundButUnusedPhrases[$phraseTitle])) { - echo "Phrase not found: {$phraseTitle}\n"; - $good = false; - } - - if ($good) { - $foundButUnusedPhrase = $foundButUnusedPhrases[$phraseTitle]; - if (!empty($foundButUnusedPhrase['addon_id']) AND $foundButUnusedPhrase['addon_id'] !== 'XenForo') { - echo "Phrase from another add-on: {$phraseTitle} ({$foundButUnusedPhrase['addon_id']})\n"; - $good = false; - } - } - - if (!$good) { - $phraseUsages = self::getPhraseUsages($phraseTitle); - foreach ($phraseUsages as $phraseUsagePath => $phraseUsageLine) { - echo " Used in {$phraseUsagePath}:{$phraseUsageLine}\n"; - } - } - } - } - } - - public static function getFoundPhraseTitles() - { - return array_keys(self::$_foundPhraseTitles); - } - - public static function getPhraseUsages($phraseTitle) - { - if (isset(self::$_foundPhraseTitles[$phraseTitle])) { - return self::$_foundPhraseTitles[$phraseTitle]; - } else { - return array(); - } - } - - public static function parsePhpForPhraseTracking($path, $contents) - { - if (self::$_lookingForPhraseTitles == false) { - return; - } - - if (strpos($path, 'library/DevHelper/') === 0) { - return; - } - - $offset = 0; - $newXenForoPhrase = 'new XenForo_Phrase('; - while (true) { - $strPos = strpos($contents, $newXenForoPhrase, $offset); - - if ($strPos !== false) { - $offset = $strPos + strlen($newXenForoPhrase); - $phraseTitle = DevHelper_Helper_Php::extractString($contents, $offset); - - if (is_string($phraseTitle)) { - self::$_foundPhraseTitles[$phraseTitle][$path] = substr_count(substr($contents, 0, $offset), "\n"); - } else { - continue; - } - } else { - break; - } - } - } - - public static function parseXmlForPhraseTracking($path) - { - if (self::$_lookingForPhraseTitles == false) { - return; - } - - $contents = file_get_contents($path); - - $offset = 0; - while (true) { - if (preg_match( - '/{xen:phrase\s+(?[^,}]+)(,|})/', - $contents, - $matches, - PREG_OFFSET_CAPTURE, - $offset - )) { - $phraseTitle = $matches['title'][0]; - self::$_foundPhraseTitles[$phraseTitle][$path] = substr_count(substr($contents, 0, $offset), "\n"); - $offset = $matches[0][1] + strlen($matches[0][0]); - } else { - break; - } - } - } -} diff --git a/library/DevHelper/Helper/ShippableHelper.php b/library/DevHelper/Helper/ShippableHelper.php deleted file mode 100644 index 2d87e64..0000000 --- a/library/DevHelper/Helper/ShippableHelper.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php - -class DevHelper_Helper_ShippableHelper -{ - public static function getVersionId($class, $path, $contents) - { - if (XenForo_Application::debugMode()) { - return filemtime($path); - } - - if (preg_match( - '#/\*\*.+?Class ' - . preg_quote($class, '#') - . '.+?@version (?<version>\d+)\s.+?\*/#s', - $contents, - $matches - )) { - return intval($matches['version']); - } else { - return false; - } - } - - public static function update($targetClass, $targetPath, $sourceClass, $sourcesContents) - { - $targetContents = str_replace($sourceClass, $targetClass, $sourcesContents); - - $php = '<?php'; - $pos = utf8_strpos($targetContents, $php); - if ($pos !== false) { - $replacement = sprintf("%s\n\n// updated by %s at %s", $php, __CLASS__, date('c')); - $targetContents = utf8_substr_replace($targetContents, $replacement, $pos, utf8_strlen($php)); - } - - $classPrefix = substr($targetClass, 0, strpos($targetClass, 'ShippableHelper_')); - $offset = 0; - while (true) { - if (!preg_match( - '#DevHelper_Helper_ShippableHelper_[a-zA-Z_]+#', - $targetContents, - $matches, - PREG_OFFSET_CAPTURE, - $offset - ) - ) { - break; - } - - $siblingSourceClass = $matches[0][0]; - $offset = $matches[0][1]; - $siblingTargetClass = str_replace('DevHelper_Helper_', $classPrefix, $siblingSourceClass); - $targetContents = substr_replace( - $targetContents, - $siblingTargetClass, - $offset, - strlen($siblingSourceClass) - ); - - class_exists($siblingTargetClass); - - $offset += 1; - } - - $targetContents = preg_replace( - '#\* @version \d+\s*\n#', - '$0 * @see ' . $sourceClass . "\n", - $targetContents, - -1, - $count - ); - - return DevHelper_Generator_File::filePutContents($targetPath, $targetContents); - } - - public static function checkForUpdate($path) - { - if (strpos($path, '/ShippableHelper/') !== false) { - $contents = DevHelper_Generator_File::fileGetContents($path); - - if (preg_match('#class\s(?<class>[^\s]+_ShippableHelper_[^\s]+)\s#', $contents, $matches)) { - class_exists($matches['class']); - } - } - } -} diff --git a/library/DevHelper/Helper/ShippableHelper/Crypt.php b/library/DevHelper/Helper/ShippableHelper/Crypt.php deleted file mode 100644 index bf7e9d5..0000000 --- a/library/DevHelper/Helper/ShippableHelper/Crypt.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php - -/** - * Class DevHelper_Helper_ShippableHelper_Crypt - * @version 3 - */ -class DevHelper_Helper_ShippableHelper_Crypt -{ - const ALGO_AES_128 = 'aes128'; - const ALGO_AES_256 = 'aes256'; - - const OPENSSL_METHOD_AES128 = 'aes-128-ecb'; - const OPENSSL_METHOD_AES256 = 'aes-256-cbc'; - - public static function encrypt($data, $key = null, $algo = null) - { - if ($key === null) { - $key = XenForo_Application::getConfig()->get('globalSalt'); - } - - switch ($algo) { - case self::ALGO_AES_128: - return self::_aes128_encrypt($data, $key); - default: - case self::ALGO_AES_256: - return self::_aes256_encrypt($data, $key); - } - } - - public static function decrypt($data, $key = null, $algo = null) - { - if ($key === null) { - $key = XenForo_Application::getConfig()->get('globalSalt'); - } - - if ($algo === null) { - if (substr($data, 0, strlen(self::ALGO_AES_256)) === self::ALGO_AES_256) { - $algo = self::ALGO_AES_256; - } else { - $algo = self::ALGO_AES_128; - } - } - - switch ($algo) { - case self::ALGO_AES_128: - return self::_aes128_decrypt($data, $key); - default: - case self::ALGO_AES_256: - return self::_aes256_decrypt($data, $key); - } - } - - /** - * Legacy AES 128 encryption. - * Supports both OpenSSL and mcrypt. - * Warning: This method is insecure and potentially dangerous, should be avoided for new application. - * - * @param string $data - * @param string $key - * @return string - */ - protected static function _aes128_encrypt($data, $key) - { - if (function_exists('openssl_encrypt')) { - $key = md5($key, true); - return openssl_encrypt($data, self::OPENSSL_METHOD_AES128, $key, OPENSSL_RAW_DATA); - } - - if (function_exists('mcrypt_encrypt')) { - $key = md5($key, true); - $padding = 16 - (strlen($data) % 16); - $data .= str_repeat(chr($padding), $padding); - /** @noinspection PhpDeprecationInspection */ - return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_ECB); - } - - return $data; - } - - /** - * Legacy AES 128 decryption. - * Supports both OpenSSL and mcrypt. - * Warning: This method is insecure and potentially dangerous, should be avoided for new application. - * - * @param string $data - * @param string $key - * @return string - */ - protected static function _aes128_decrypt($data, $key) - { - if (function_exists('openssl_decrypt')) { - $key = md5($key, true); - return openssl_decrypt($data, self::OPENSSL_METHOD_AES128, $key, OPENSSL_RAW_DATA); - } - - if (function_exists('mcrypt_decrypt')) { - $key = md5($key, true); - /** @noinspection PhpDeprecationInspection */ - $data = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_ECB); - $padding = ord($data[strlen($data) - 1]); - return substr($data, 0, -$padding); - } - - return $data; - } - - /** - * Secure AES 256 encryption. - * This method only supports OpenSSL (unlike the AES 128 variant which also supports mcrypt). - * - * @param string $data - * @param string $key - * @return string - */ - protected static function _aes256_encrypt($data, $key) - { - if (function_exists('mb_substr') && function_exists('openssl_encrypt')) { - $ivLength = openssl_cipher_iv_length(self::OPENSSL_METHOD_AES256); - $iv = openssl_random_pseudo_bytes($ivLength); - $encrypted = openssl_encrypt($data, self::OPENSSL_METHOD_AES256, $key, OPENSSL_RAW_DATA, $iv); - - return self::ALGO_AES_256 . $iv . $encrypted; - } - - return $data; - } - - /** - * Secure AES 256 decryption. - * This method only supports OpenSSL (unlike the AES 128 variant which also supports mcrypt). - * - * @param string $data - * @param string $key - * @return string - */ - protected static function _aes256_decrypt($data, $key) - { - if (function_exists('mb_substr') && function_exists('openssl_encrypt')) { - $prefixLength = mb_strlen(self::ALGO_AES_256, '8bit'); - $prefix = mb_substr($data, 0, $prefixLength); - if ($prefix === self::ALGO_AES_256) { - $ivLength = openssl_cipher_iv_length(self::OPENSSL_METHOD_AES256); - $iv = mb_substr($data, $prefixLength, $ivLength, '8bit'); - $encrypted = mb_substr($data, $prefixLength + $ivLength, null, '8bit'); - - return openssl_decrypt($encrypted, self::OPENSSL_METHOD_AES256, $key, OPENSSL_RAW_DATA, $iv); - } - } - - return $data; - } - -} diff --git a/library/DevHelper/Helper/ShippableHelper/DateTime.php b/library/DevHelper/Helper/ShippableHelper/DateTime.php deleted file mode 100644 index 433aa65..0000000 --- a/library/DevHelper/Helper/ShippableHelper/DateTime.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php - -/** - * Class DevHelper_Helper_ShippableHelper_DateTime - * @version 3 - */ -class DevHelper_Helper_ShippableHelper_DateTime -{ - public static function tzOffsetToName($offset, $isDst = null) - { - if ($isDst === null) { - $isDst = date('I'); - } - - $zone = timezone_name_from_abbr('', $offset, $isDst); - - if ($zone === false) { - foreach (timezone_abbreviations_list() as $abbr) { - foreach ($abbr as $city) { - if ((bool)$city['dst'] === (bool)$isDst && - strlen($city['timezone_id']) > 0 && - $city['offset'] == $offset - ) { - $zone = $city['timezone_id']; - break; - } - } - - if ($zone !== false) { - break; - } - } - } - - return $zone; - } - - /** - * Get Unix timestamp in GMT for inputted date values. - * - * @param int|array $year in user timezone - * @param int $month in user timezone - * @param int $day in user timezone - * @param int $hour in user timezone - * @param int $minute in user timezone - * @param int $second in user timezone - * @param int|null $offset custom offset to override user's - * - * @return int timestamp in GMT - */ - public static function gmmktime($year, $month = 0, $day = 0, $hour = 0, $minute = 0, $second = 0, $offset = null) - { - if (is_array($year)) { - $args = func_get_args(); - if (count($args) !== 1) { - return 0; - } - - $values = $year; - $year = 0; - foreach (array('year', 'month', 'day', 'hour', 'minute', 'second', 'offset') as $key) { - if (isset($values[$key]) - && is_int($values[$key]) - ) { - $$key = intval($values[$key]); - } - } - } - - $timestamp = gmmktime($hour, $minute, $second, $month, $day, $year); - - if ($offset === null) { - $offset = XenForo_Locale::getTimeZoneOffset(); - } - - return $timestamp - $offset; - } - - /** - * Get timezone offset by string or object - * - * @param string|DateTimeZone $timeZoneString String time zone to override user timezone - * @return int - */ - public static function getTimeZoneOffset($timeZoneString) - { - $dt = new DateTime('@' . XenForo_Application::$time); - - if ($timeZoneString instanceof DateTimeZone) { - $timeZone = $timeZoneString; - } else { - $timeZone = new DateTimeZone($timeZoneString); - } - $dt->setTimezone($timeZone); - - return $dt->getOffset(); - } - -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/Html.php b/library/DevHelper/Helper/ShippableHelper/Html.php deleted file mode 100644 index 5f65eb9..0000000 --- a/library/DevHelper/Helper/ShippableHelper/Html.php +++ /dev/null @@ -1,399 +0,0 @@ -<?php - -/** - * Class DevHelper_Helper_ShippableHelper_Html - * @version 18 - */ -class DevHelper_Helper_ShippableHelper_Html -{ - public static function viewSnippet($string, array $params) - { - return self::snippet($string, 0, $params); - } - - public static function preSnippet(array &$message, XenForo_BbCode_Parser $parser, array $options = array()) - { - $options = array_merge(array( - 'previewBreakBbCode' => 'prbreak', - 'messageKey' => 'message', - ), $options); - - $previewFound = false; - $tagOpen = ''; - $tagClose = ''; - - if (!empty($options['previewBreakBbCode'])) { - $textRef =& $message[$options['messageKey']]; - $tagOpen = '[' . $options['previewBreakBbCode'] . ']'; - $posOpen = stripos($textRef, $tagOpen); - if ($posOpen !== false) { - $tagClose = '[/' . $options['previewBreakBbCode'] . ']'; - $posClose = stripos($textRef, $tagClose, $posOpen); - if ($posClose !== false) { - $previewFound = true; - $previewTextOffset = $posOpen + strlen($tagOpen); - $previewTextLength = $posClose - $posOpen - strlen($tagOpen); - - if ($previewTextLength > 0) { - $textRef = substr($textRef, $previewTextOffset, $previewTextLength); - - } else { - $textRef = substr($textRef, 0, $posOpen); - } - } - } - } - - $messageHtml = XenForo_ViewPublic_Helper_Message::getBbCodeWrapper( - $message, $parser, $options); - - if ($previewFound) { - $messageHtml = sprintf('%s%s%s', $tagOpen, $messageHtml, $tagClose); - } - - return $messageHtml; - } - - /** - * @param string $string - * @param int $maxLength - * @param array $options - * - * @return string - */ - public static function snippet($string, $maxLength = 0, array $options = array()) - { - $options = array_merge(array( - 'context' => '', - 'ellipsis' => '…', - 'fromStart' => true, - 'previewBreakBbCode' => 'prbreak', - 'processImage' => true, - 'processLink' => true, - 'processFrame' => true, - 'processScript' => true, - 'replacements' => array(), - 'stripImage' => false, - 'stripLink' => false, - 'stripFrame' => false, - 'stripScript' => false, - 'stripSpacing' => true, - - // WARNING: options below are for internal usage only - '_isPreview' => false, - ), $options); - if (!isset($options['maxLength'])) { - $options['maxLength'] = $maxLength; - } - - self::snippetPreProcess($string, $options); - if (!empty($options['_isPreview'])) { - $snippet = $string; - } else { - $snippet = self::snippetCallHelper($string, $options); - self::snippetFixBrokenHtml($snippet, $options); - } - - if (!empty($options['context'])) { - self::snippetOnContext($snippet, $options['context']); - } - - if ($snippet === '') { - $plainTextString = utf8_trim(strip_tags($string)); - if ($plainTextString !== '') { - $snippet = self::snippetCallHelper($plainTextString, $options); - } - } - - self::snippetPostProcess($snippet, $options); - - return $snippet; - } - - public static function snippetPreProcess(&$string, array &$options) - { - if (!empty($options['previewBreakBbCode']) - && preg_match(sprintf('#\[%1$s\](?<' . 'preview>.*)\[/%1$s\]#si', - preg_quote($options['previewBreakBbCode'], '#')), - $string, $matches, PREG_OFFSET_CAPTURE) - ) { - // preview break bbcode found - if (!empty($matches['preview'][0])) { - // preview text specified, use it directly - $string = $matches['preview'][0]; - $options['maxLength'] = 0; - $options['_isPreview'] = true; - } else { - // use content before the found bbcode to continue - $string = substr($string, 0, $matches[0][1]); - $options['maxLength'] = 0; - } - } - - $string = str_replace("\n", ' ', $string); - $string = preg_replace('#<br\s?\/?>#', "\n", $string); - $string = str_replace('​', '', $string); - - if (!empty($options['_isPreview'])) { - return; - } - - $replacementsRef =& $options['replacements']; - $replacementTags = array(); - if (!!$options['processImage'] && !$options['stripImage']) { - $replacementTags[] = 'img'; - } - if (!!$options['processLink'] && !$options['stripLink']) { - $replacementTags[] = 'a'; - } - if (!!$options['processFrame'] && !$options['stripFrame']) { - $replacementTags[] = 'iframe'; - } - if (!!$options['processScript'] && !$options['stripScript']) { - $replacementTags[] = 'script'; - } - if (count($replacementTags) > 0) { - $replacementOffset = 0; - $replacementRegEx = sprintf('#<(%s)(\s[^>]*)?>.*?</\\1>#i', implode('|', $replacementTags)); - while (true) { - if (!preg_match($replacementRegEx, $string, - $replacementMatches, PREG_OFFSET_CAPTURE, $replacementOffset) - ) { - break; - } - $replacement = array( - 'original' => $replacementMatches[0][0], - 'replacement' => sprintf('<br data-replacement-id="%d" />', count($replacementsRef)), - ); - $replacementsRef[] = $replacement; - $string = str_replace($replacement['original'], $replacement['replacement'], $string); - $replacementOffset = $replacementMatches[0][1] + 1; - } - } - - if (!!$options['stripImage']) { - $string = preg_replace('#<img[^>]+/>#', '', $string); - } - if (!!$options['stripLink']) { - $string = preg_replace('#<a[^>]+>(.+?)</a>#', '$1', $string); - } - if (!!$options['stripFrame']) { - $string = preg_replace('#<iframe[^>]+></iframe>#', '', $string); - } - if (!!$options['stripScript']) { - $string = preg_replace('#<script[^>]+>.*?</script>#', '', $string); - } - if (!!$options['stripSpacing']) { - $string = preg_replace('#(\n\s*)+#', "\n", $string); - } - } - - public static function snippetCallHelper($string, array &$options) - { - $snippet = XenForo_Template_Helper_Core::callHelper('snippet', array($string, $options['maxLength'], $options)); - - // TODO: find better way to avoid having to call this to reset snippet - $snippet = htmlspecialchars_decode($snippet); - $snippet = preg_replace('#\.\.\.\z#', '', $snippet); - - return $snippet; - } - - public static function snippetFixBrokenHtml(&$snippet, array &$options) - { - $offset = 0; - $stack = array(); - while (true) { - $startPos = utf8_strpos($snippet, '<', $offset); - if ($startPos !== false) { - $endPos = utf8_strpos($snippet, '>', $startPos); - if ($endPos === false) { - // we found a partial open tag, best to delete the whole thing - $snippet = utf8_substr($snippet, 0, $startPos); - break; - } - - $foundLength = $endPos - $startPos - 1; - $found = utf8_substr($snippet, $startPos + 1, $foundLength); - $offset = $endPos; - - if (preg_match('#^(?<closing>/?)(?<tag>\w+)#', $found, $matches)) { - $tag = $matches['tag']; - $isClosing = !empty($matches['closing']); - $isSelfClosing = (!$isClosing && (utf8_substr($found, $foundLength - 1, 1) === '/')); - - if ($isClosing) { - $lastInStack = null; - $lastInStackTag = null; - if (count($stack) > 0) { - $lastInStack = array_pop($stack); - $lastInStackTag = $lastInStack['tag']; - } - - if ($lastInStackTag !== $tag) { - // found tag does not match the one in stack - $replacement = ''; - - // first we have to close the one in stack - if ($lastInStackTag !== null) { - $replacement .= sprintf('</%s>', $tag); - } - - // then we have to self close the found tag - $replacement .= utf8_substr($snippet, $startPos, $endPos - $startPos - 1); - $replacement .= '/>'; - - // do the replacement - $snippet = utf8_substr_replace($snippet, $replacement, $startPos, $endPos - $startPos); - $offset = $startPos + utf8_strlen($snippet); - } - } elseif ($isSelfClosing) { - // do nothing - } else { - // is opening tag - $stack[] = array('tag' => $tag, 'offset' => $startPos, 'till' => $endPos); - } - } - } else { - break; - } - } - - // close any remaining tags - while (!empty($stack)) { - $stackItem = array_pop($stack); - if (utf8_strlen($snippet) === $stackItem['till'] + 1) { - // the latest tag is empty, delete it - $snippet = utf8_substr($snippet, 0, $stackItem['offset']); - continue; - } - - self::snippetAppendEllipsis($snippet, $options); - $snippet .= sprintf('</%s>', $stackItem['tag']); - } - } - - public static function snippetAppendEllipsis(&$snippet, array &$options) - { - if (empty($options['ellipsis'])) { - return; - } - - if (!preg_match('#<\/?(div|iframe|img|li|ol|p|script|ul)[^>]*>\z#', $snippet)) { - $snippet .= $options['ellipsis']; - $options['ellipsis'] = ''; - } - } - - public static function snippetOnContext(&$snippet, $context) - { - switch ($context) { - case 'link': - if (strpos($snippet, '<a ') !== false) { - $snippet = ''; - } - break; - } - } - - public static function snippetPostProcess(&$snippet, array &$options) - { - // strip all empty body tags - $snippetBinaryLength = 0; - while (strlen($snippet) !== $snippetBinaryLength) { - $snippetBinaryLength = strlen($snippet); - $snippet = preg_replace('#<(\w+)(\s[^>]+)?>\s*<\/\\1>#', '', $snippet); - } - - // restore replacements - foreach ($options['replacements'] as $replacement) { - $snippet = str_replace($replacement['replacement'], $replacement['original'], $snippet); - } - - // remove invisible characters - $snippet = utf8_trim($snippet); - - // restore line breaks - $snippet = nl2br($snippet); - - self::snippetAppendEllipsis($snippet, $options); - - $snippet = utf8_trim($snippet); - } - - public static function stripFont($html) - { - $html = preg_replace('#(<[^>]+)( style="[^"]+")([^>]*>)#', '$1$3', $html); - $html = preg_replace('#<\/?(b|i)>#', '', $html); - - return $html; - } - - public static function getMetaTags($html) - { - $tags = array(); - - $headPos = strpos($html, '</head>'); - if ($headPos === false) { - return $tags; - } - - $head = substr($html, 0, $headPos); - - $offset = 0; - while (true) { - if (preg_match('#<meta[^>]+>#i', $head, $matches, PREG_OFFSET_CAPTURE, $offset)) { - $tag = $matches[0][0]; - $offset = $matches[0][1] + strlen($tag); - $name = null; - $value = null; - - if (preg_match('#(name|property)="(?<name>[^"]+)"#i', $tag, $matches)) { - $name = $matches['name']; - } elseif (preg_match('#itemprop="(?<itemprop>[^"]+)"#i', $tag, $matches)) { - $name = $matches['itemprop']; - } else { - continue; - } - - if (preg_match('#content="(?<value>[^"]+)"#', $tag, $matches)) { - $value = self::entityDecode($matches['value']); - } else { - continue; - } - - $tags[] = array( - 'name' => $name, - 'value' => $value, - ); - } else { - break; - } - } - - return $tags; - } - - public static function getTitleTag($html) - { - if (preg_match('#<title>(?<title>[^<]+)#i', $html, $matches)) { - return self::entityDecode($matches['title']); - } - - return ''; - } - - public static function entityDecode($html) - { - $decoded = $html; - - // required to deal with " etc. - $decoded = html_entity_decode($decoded, ENT_COMPAT, 'UTF-8'); - - // required to deal with Ӓ etc. - $convmap = array(0x0, 0x2FFFF, 0, 0xFFFF); - $decoded = mb_decode_numericentity($decoded, $convmap, 'UTF-8'); - - return $decoded; - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/Http.php b/library/DevHelper/Helper/ShippableHelper/Http.php deleted file mode 100644 index 30f10a8..0000000 --- a/library/DevHelper/Helper/ShippableHelper/Http.php +++ /dev/null @@ -1,110 +0,0 @@ - 3600, - ), $options); - - $cache = null; - $cacheKey = ''; - if ($options['cacheTtl'] > 0) { - $cache = XenForo_Application::getCache(); - } - if ($cache) { - $cacheKey = self::_resolveRedirect_getCacheKey($url); - $resolved = $cache->load($cacheKey, false, true); - if (!empty($resolved)) { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__METHOD__, sprintf('Resolved via cache. ($url=%s, $limit=%d) -> %s', - $url, $limit, $resolved)); - } - - return unserialize($resolved); - } - } - - $resolved = self::_resolveRedirect_curl($url, $limit); - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__METHOD__, sprintf('Resolved via curl. ($url=%s, $limit=%d) -> %s', - $url, $limit, serialize($resolved))); - } - - if (!empty($cache) - && !empty($cacheKey) - ) { - $cache->save(serialize($resolved), $cacheKey, array(), $options['cacheTtl']); - } - - return $resolved; - } - - protected static function _resolveRedirect_curl($url, $limit, array $options = array()) - { - if (!isset($options['originalUrl'])) { - $options['originalUrl'] = $url; - } - if (!isset($options['originalLimit'])) { - $options['originalLimit'] = $limit; - } - - if (!parse_url($url, PHP_URL_HOST)) { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__METHOD__, sprintf('Cannot resolve malformed url. ($url=%s, $limit=%d) -> %s', - $options['originalUrl'], $options['originalLimit'], $url)); - } - - return $url; - } - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_NOBODY, true); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $headers = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - $redirectLocation = ''; - if ($httpCode >= 300 - && $httpCode < 400 - && preg_match('#Location: (?http.+)\s#', $headers, $matches) - ) { - $redirectLocation = $matches['location']; - } - - if ($redirectLocation === '') { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__METHOD__, sprintf('Resolved. ($url=%s, $limit=%d) -> %s', - $options['originalUrl'], $options['originalLimit'], $url)); - } - - return $url; - } - - if ($limit === -1) { - $nextLimit = -1; - } elseif ($limit > 0) { - $nextLimit = $limit - 1; - } else { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__METHOD__, sprintf('Too many redirects! ($url=%s, $limit=%d) -> %s', - $options['originalUrl'], $options['originalLimit'], $url)); - } - - return $url; - } - - return self::_resolveRedirect_curl($redirectLocation, $nextLimit, $options); - } - - protected static function _resolveRedirect_getCacheKey($url) - { - return preg_replace('#[^A-Za-z0-9_]#', '', sprintf('%s_%s', $url, md5(sprintf('%s_%s', $url, __CLASS__)))); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/Image.php b/library/DevHelper/Helper/ShippableHelper/Image.php deleted file mode 100644 index 52eda51..0000000 --- a/library/DevHelper/Helper/ShippableHelper/Image.php +++ /dev/null @@ -1,77 +0,0 @@ - 0 && $height > 0) { - $size = $width; - $mode = $height; - } elseif ($width > 0) { - $size = $width; - $mode = bdImage_Integration::MODE_STRETCH_HEIGHT; - } elseif ($height > 0) { - $size = $height; - $mode = bdImage_Integration::MODE_STRETCH_WIDTH; - } - - return bdImage_Integration::buildThumbnailLink($imageUrl, $size, $mode); - } - - $thumbnailPath = self::getThumbnailPath($imageUrl, $width, $height, $dir); - $thumbnailUrl = XenForo_Link::convertUriToAbsoluteUri(XenForo_Application::$externalDataUrl - . self::_getThumbnailRelativePath($imageUrl, $width, $height, $dir), true); - if (file_exists($thumbnailPath)) { - $thumbnailFileSize = filesize($thumbnailPath); - if ($thumbnailFileSize > 0) { - return sprintf('%s?fs=%d', $thumbnailUrl, $thumbnailFileSize); - } - } - - $coreData = DevHelper_Helper_ShippableHelper_ImageCore::open($imageUrl); - $coreData = DevHelper_Helper_ShippableHelper_ImageCore::thumbnail($coreData, $width, $height); - DevHelper_Helper_ShippableHelper_ImageCore::save($coreData, $thumbnailPath); - - return sprintf('%s?t=%d', $thumbnailUrl, XenForo_Application::$time); - } - - public static function getThumbnailPath($imageUrl, $width, $height = 0, $dir = null) - { - $thumbnailPath = XenForo_Helper_File::getExternalDataPath() - . self::_getThumbnailRelativePath($imageUrl, $width, $height, $dir); - - return $thumbnailPath; - } - - protected static function _getThumbnailRelativePath($imageUrl, $width, $height, $dir) - { - $path = parse_url($imageUrl, PHP_URL_PATH); - $basename = basename($path); - if (empty($basename)) { - $basename = md5($imageUrl); - } - $fileName = preg_replace('#[^a-zA-Z0-9]#', '', $basename) . md5(serialize(array( - 'fullPath' => $imageUrl, - 'width' => $width, - 'height' => $height, - ))); - $ext = XenForo_Helper_File::getFileExtension($basename); - if (!in_array($ext, array('gif', 'jpeg', 'jpg', 'png'), true)) { - $ext = 'jpg'; - } - $divider = substr(md5($fileName), 0, 2); - - if (empty($dir)) { - $dir = trim(str_replace('_', '/', substr(__CLASS__, 0, strpos(__CLASS__, '_ShippableHelper_Image'))), '/'); - } - - return sprintf('/%s/%sx%s/%s/%s.%s', $dir, $width, $height, $divider, $fileName, $ext); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/ImageCore.php b/library/DevHelper/Helper/ShippableHelper/ImageCore.php deleted file mode 100644 index 2372a54..0000000 --- a/library/DevHelper/Helper/ShippableHelper/ImageCore.php +++ /dev/null @@ -1,330 +0,0 @@ -get('maxImageResizePixelCount') - ) { - $image = XenForo_Image_Abstract::createFromFile($accessiblePath, $imageInfo['typeInt']); - } - - return array( - 'image' => $image, - 'imageInfo' => $imageInfo, - ); - } - - /** - * @param array $data - * @param string $path - * @return bool - */ - public static function save(array $data, $path) - { - XenForo_Helper_File::createDirectory(dirname($path), true); - $outputPath = tempnam(XenForo_Helper_File::getTempDir(), __CLASS__); - - /** @noinspection PhpUndefinedMethodInspection */ - $data['image']->output($data['imageInfo']['typeInt'], $outputPath); - return XenForo_Helper_File::safeRename($outputPath, $path); - } - - /** - * @param array $data - * @param int $width - * @param int $height - * @return array() - */ - public static function thumbnail(array $data, $width, $height = 0) - { - $image = $data['image']; - $imageInfo = $data['imageInfo']; - - if (empty($image)) { - // no image input, create a new one - $size = min(100, max($width, $height)); - $image = XenForo_Image_Abstract::createImage($size, $size); - $imageInfo['typeInt'] = IMAGETYPE_PNG; - } - - $isGd = $image instanceof XenForo_Image_Gd; - $isImageMagick = $image instanceof XenForo_Image_ImageMagick_Pecl; - if ($imageInfo['typeInt'] === IMAGETYPE_GIF && $isImageMagick) { - /** @noinspection PhpParamsInspection */ - _DevHelper_Helper_ShippableHelper_ImageCore_ImageMagick::dropFrames($image); - } - - if (in_array($width, array('', 0), true) && $height > 0) { - self::_resizeStretchWidth($image, $height); - } elseif (in_array($height, array('', 0), true) && $width > 0) { - self::_resizeStretchHeight($image, $width); - } elseif ($width > 0 && $height > 0) { - self::_cropExact($image, $width, $height); - } elseif ($width > 0) { - self::_cropSquare($image, $width); - } - - if ($imageInfo['typeInt'] === IMAGETYPE_JPEG) { - if ($isGd) { - /** @noinspection PhpParamsInspection */ - _DevHelper_Helper_ShippableHelper_ImageCore_Gd::progressiveJpeg($image); - } elseif ($isImageMagick) { - /** @noinspection PhpParamsInspection */ - _DevHelper_Helper_ShippableHelper_ImageCore_ImageMagick::progressiveJpeg($image); - } - } - - return array( - 'image' => $image, - 'imageInfo' => $imageInfo, - ); - } - - /** - * @param XenForo_Image_Abstract $imageObj - * @param int $targetHeight - */ - protected static function _resizeStretchWidth(XenForo_Image_Abstract $imageObj, $targetHeight) - { - $targetWidth = $targetHeight / $imageObj->getHeight() * $imageObj->getWidth(); - $imageObj->thumbnail($targetWidth, $targetHeight); - } - - /** - * @param XenForo_Image_Abstract $imageObj - * @param int $targetWidth - */ - protected static function _resizeStretchHeight(XenForo_Image_Abstract $imageObj, $targetWidth) - { - $targetHeight = $targetWidth / $imageObj->getWidth() * $imageObj->getHeight(); - $imageObj->thumbnail($targetWidth, $targetHeight); - } - - /** - * @param XenForo_Image_Abstract $imageObj - * @param int $targetWidth - * @param int $targetHeight - */ - protected static function _cropExact(XenForo_Image_Abstract $imageObj, $targetWidth, $targetHeight) - { - $origRatio = $imageObj->getWidth() / $imageObj->getHeight(); - $cropRatio = $targetWidth / $targetHeight; - if ($origRatio > $cropRatio) { - $thumbnailHeight = $targetHeight; - $thumbnailWidth = $thumbnailHeight * $origRatio; - } else { - $thumbnailWidth = $targetWidth; - $thumbnailHeight = $thumbnailWidth / $origRatio; - } - - if ($thumbnailWidth <= $imageObj->getWidth() - && $thumbnailHeight <= $imageObj->getHeight() - ) { - $imageObj->thumbnail($thumbnailWidth, $thumbnailHeight); - self::_cropCenter($imageObj, $targetWidth, $targetHeight); - } else { - if ($origRatio > $cropRatio) { - self::_cropCenter($imageObj, $imageObj->getHeight() * $cropRatio, $imageObj->getHeight()); - } else { - self::_cropCenter($imageObj, $imageObj->getWidth(), $imageObj->getWidth() / $cropRatio); - } - } - } - - /** - * @param XenForo_Image_Abstract $imageObj - * @param int $target - */ - protected static function _cropSquare(XenForo_Image_Abstract $imageObj, $target) - { - $imageObj->thumbnailFixedShorterSide($target); - self::_cropCenter($imageObj, $target, $target); - } - - /** - * @param XenForo_Image_Abstract $imageObj - * @param int $cropWidth - * @param int $cropHeight - */ - protected static function _cropCenter(XenForo_Image_Abstract $imageObj, $cropWidth, $cropHeight) - { - if (self::$cropTopLeft) { - // revert to top left cropping (old version behavior) - /** @noinspection PhpParamsInspection */ - $imageObj->crop(0, 0, $cropWidth, $cropHeight); - return; - } - - $width = $imageObj->getWidth(); - $height = $imageObj->getHeight(); - $x = floor(($width - $cropWidth) / 2); - $y = floor(($height - $cropHeight) / 2); - - /** @noinspection PhpParamsInspection */ - $imageObj->crop($x, $y, $cropWidth, $cropHeight); - } - - protected static function _getAccessiblePath($path) - { - if (!parse_url($path, PHP_URL_HOST)) { - return $path; - } - - $boardUrl = XenForo_Application::getOptions()->get('boardUrl'); - if (strpos($path, '..') === false - && strpos($path, $boardUrl) === 0 - ) { - $localFilePath = self::_getLocalFilePath(substr($path, strlen($boardUrl))); - if (self::_imageExists($localFilePath)) { - return $localFilePath; - } - } - - if (preg_match('#attachments/(.+\.)*(?\d+)/#', $path, $matches)) { - $fullIndex = XenForo_Link::buildPublicLink('full:index'); - $canonicalIndex = XenForo_Link::buildPublicLink('canonical:index'); - if (strpos($path, $fullIndex) === 0 || strpos($path, $canonicalIndex) === 0) { - $attachmentDataFilePath = self::_getAttachmentDataFilePath($matches['id']); - if (self::_imageExists($attachmentDataFilePath)) { - return $attachmentDataFilePath; - } - } - } - - return DevHelper_Helper_ShippableHelper_TempFile::download($path); - } - - protected static function _getLocalFilePath($path) - { - // remove query parameters - $path = preg_replace('#(\?|\#).*$#', '', $path); - if (strlen($path) === 0) { - return $path; - } - - $extension = XenForo_Helper_File::getFileExtension($path); - if (!in_array($extension, array('gif', 'jpeg', 'jpg', 'png'), true)) { - return ''; - } - - /** @var XenForo_Application $app */ - $app = XenForo_Application::getInstance(); - $path = sprintf('%s/%s', rtrim($app->getRootDir(), '/'), ltrim($path, '/')); - - return $path; - } - - protected static function _getAttachmentDataFilePath($attachmentId) - { - /** @var XenForo_Model_Attachment $attachmentModel */ - static $attachmentModel = null; - static $attachments = array(); - - if ($attachmentModel === null) { - $attachmentModel = XenForo_Model::create('XenForo_Model_Attachment'); - } - - if (!isset($attachments[$attachmentId])) { - $attachments[$attachmentId] = $attachmentModel->getAttachmentById($attachmentId); - } - - if (empty($attachments[$attachmentId])) { - return ''; - } - - return $attachmentModel->getAttachmentDataFilePath($attachments[$attachmentId]); - } - - protected static function _imageExists($path) - { - if (!is_string($path) || strlen($path) === 0) { - // invalid path - return false; - } - - $pathSize = @filesize($path); - if (!is_int($pathSize)) { - // file not exists - return false; - } - - // according to many sources, no valid image can be smaller than 14 bytes - // http://garethrees.org/2007/11/14/pngcrush/ - // https://github.com/mathiasbynens/small/blob/master/gif.gif - return $pathSize >= 14; - } - - protected static function _getImageInfo($path) - { - $imageInfo = array(); - - $fileSize = @filesize($path); - if (!empty($fileSize)) { - $imageInfo['fileSize'] = $fileSize; - } - - if (!empty($imageInfo['fileSize']) - && $info = @getimagesize($path) - ) { - $imageInfo['width'] = $info[0]; - $imageInfo['height'] = $info[1]; - $imageInfo['typeInt'] = $info[2]; - } - - return $imageInfo; - } -} - -class _DevHelper_Helper_ShippableHelper_ImageCore_Gd extends XenForo_Image_Gd -{ - public static function progressiveJpeg(XenForo_Image_Gd $obj) - { - imageinterlace($obj->_image, 1); - } -} - -class _DevHelper_Helper_ShippableHelper_ImageCore_ImageMagick extends XenForo_Image_ImageMagick_Pecl -{ - public static function dropFrames(XenForo_Image_ImageMagick_Pecl $obj) - { - $image = $obj->_image; - - if ($image->count() > 1) { - $newImage = null; - - foreach ($image as $frame) { - $newImage = new Imagick(); - $newImage->addImage($frame->getImage()); - break; - } - - if ($newImage !== null) { - $obj->_image = $newImage; - $image->destroy(); - } - } - } - - public static function progressiveJpeg(XenForo_Image_ImageMagick_Pecl $obj) - { - $obj->_image->setInterlaceScheme(Imagick::INTERLACE_PLANE); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/ImageSize.php b/library/DevHelper/Helper/ShippableHelper/ImageSize.php deleted file mode 100644 index 2067660..0000000 --- a/library/DevHelper/Helper/ShippableHelper/ImageSize.php +++ /dev/null @@ -1,542 +0,0 @@ - 2000 - || strpos($uri, ' ') !== false - || strpos($uri, 'data:') === 0 - ) { - return array( - 'uri' => $uri, - 'width' => 0, - 'height' => 0, - 'timestamp' => time(), - ); - } - - $cacheId = __CLASS__ . md5($uri); - $cache = ($ttl > 0 ? XenForo_Application::getCache() : null); - - if ($cache) { - $cachedData = $cache->load($cacheId); - if (is_string($cachedData)) { - $cachedData = unserialize($cachedData); - } - if (!is_array($cachedData)) { - $cachedData = array(); - } - if (!empty($cachedData['uri']) - && $cachedData['uri'] === $uri - && XenForo_Application::$time - $cachedData['timestamp'] < $ttl - ) { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('$uri=%s; CACHE HIT', $uri)); - } - return $cachedData; - } - } - - $originalUri = $uri; - $requestPaths = XenForo_Application::get('requestPaths'); - if (strpos($uri, $requestPaths['fullBasePath']) === 0) { - $uri = substr($uri, strlen($requestPaths['fullBasePath'])); - } - - $data = self::_calculate($uri); - if (empty($data['width']) - || empty($data['height']) - ) { - $absoluteUri = XenForo_Link::convertUriToAbsoluteUri($uri, true); - if ($absoluteUri != $uri) { - $data = self::_calculate($absoluteUri); - } - } - - if (!empty($data['uri']) - && $data['uri'] != $originalUri - ) { - $data['_calculateUri'] = $data['uri']; - } - $data['uri'] = $originalUri; - - if ($cache) { - $cache->save(serialize($data), $cacheId, array(), $ttl); - } - - return $data; - } - - public static function getDataUriMatchesRatio($uri, $rgba = null) - { - if (!function_exists('imagecreatetruecolor')) { - return ''; - } - - $calculated = self::calculate($uri); - if ($calculated['width'] < 1 || $calculated['height'] < 1) { - return ''; - } - - return self::getDataUriAtSize($calculated['width'], $calculated['height'], $rgba); - } - - public static function getDataUriAtSize($width, $height, $rgba = null) - { - if (!function_exists('imagecreatetruecolor') || $width < 1 || $height < 1) { - return ''; - } - - $gcd = self::_findGreatestCommonDivisor($width, $height); - $width /= $gcd; - $height /= $gcd; - - $image = imagecreate($width, $height); - imagealphablending($image, false); - imagesavealpha($image, true); - if (!is_array($rgba) || count($rgba) < 3) { - $color = imagecolorallocatealpha($image, 255, 255, 255, 127); - } else { - $rgba = array_values($rgba); - $r = max(0, min(255, $rgba[0] < 1 ? floor(255 * $rgba[0]) : $rgba[0])); - $g = max(0, min(255, $rgba[1] < 1 ? floor(255 * $rgba[1]) : $rgba[0])); - $b = max(0, min(255, $rgba[2] < 1 ? floor(255 * $rgba[2]) : $rgba[0])); - $a = isset($rgba[3]) ? max(0, min(127, $rgba[3] < 1 ? floor(127 * $rgba[3]) : $rgba[0])) : 0; - $color = imagecolorallocatealpha($image, $r, $g, $b, $a); - } - imagefilledrectangle($image, 0, 0, $width, $height, $color); - - ob_start(); - imagepng($image); - $imageBytes = ob_get_contents(); - ob_end_clean(); - - return 'data:image/png;base64,' . base64_encode($imageBytes); - } - - protected static function _calculate($uri) - { - /** @var self $instance */ - static $instance = null; - - $startTime = microtime(true); - - if (preg_match('#^(?.+)?attachments/(.+\.)*(?\d+)/#', $uri, $matches) - && ($matches['prefix'] === '' - || strpos($matches['prefix'], XenForo_Link::buildPublicLink('full:index')) === 0 - || strpos($matches['prefix'], XenForo_Link::buildPublicLink('canonical:index')) === 0 - ) - ) { - $attachmentResult = self::_calculateForAttachment($uri, $matches['id']); - if (!empty($attachmentResult['width']) - && !empty($attachmentResult['height']) - ) { - return $attachmentResult; - } - } - - if ($instance === null) { - $instance = new self($uri); - } else { - $instance->close(); - $instance->load($uri); - } - - list($width, $height) = $instance->getSize(); - - if (XenForo_Application::debugMode()) { - $elapsedTime = microtime(true) - $startTime; - XenForo_Helper_File::log(__CLASS__, sprintf('$uri=%s; $width=%d, $height=%d; $elapsedTime=%.6f', - $uri, $width, $height, $elapsedTime)); - } - - return array( - 'uri' => $uri, - 'width' => $width, - 'height' => $height, - 'timestamp' => time(), - ); - } - - protected static function _calculateForAttachment($uri, $attachmentId) - { - $startTime = microtime(true); - - /** @var XenForo_Model_Attachment $attachmentModel */ - static $attachmentModel = null; - static $attachments = array(); - - if ($attachmentModel === null) { - $attachmentModel = XenForo_Model::create('XenForo_Model_Attachment'); - } - - if (!isset($attachments[$attachmentId])) { - $attachments[$attachmentId] = $attachmentModel->getAttachmentById($attachmentId); - } - - $width = 0; - if (!empty($attachments[$attachmentId]['width'])) { - $width = intval($attachments[$attachmentId]['width']); - } - - $height = 0; - if (!empty($attachments[$attachmentId]['height'])) { - $height = intval($attachments[$attachmentId]['height']); - } - - if (XenForo_Application::debugMode()) { - $elapsedTime = microtime(true) - $startTime; - XenForo_Helper_File::log(__CLASS__, sprintf('$uri=%s; $attachmentId=%d, $width=%d, $height=%d;' - . ' $elapsedTime=%.6f', $uri, $attachmentId, $width, $height, $elapsedTime)); - } - - return array( - 'uri' => $uri, - 'attachmentId' => $attachmentId, - 'width' => $width, - 'height' => $height, - 'timestamp' => time(), - ); - } - - protected static function _findGreatestCommonDivisor($a, $b) - { - $mod = $a % $b; - if ($mod === 0) { - return $b; - } else { - return self::_findGreatestCommonDivisor($b, $mod); - } - } - - // https://github.com/tommoor/fastimage/commit/7bf53fcfebb5bc04b78a8cf23862778256de2241 - private $pos = 0; - private $str; - private $type; - private $handle; - - private function __construct($uri = null) - { - if ($uri) { - $this->load($uri); - } - } - - private function load($uri) - { - if ($this->handle) { - $this->close(); - } - - $context = null; - $host = parse_url($uri, PHP_URL_HOST); - if ($host) { - $httpContext = array('timeout' => 2.0); - $httpContext['header'] = "Referer: http://$host\r\n"; - - if (!empty($_SERVER['HTTP_USER_AGENT'])) { - $httpContext['user_agent'] = $_SERVER['HTTP_USER_AGENT']; - } else { - $httpContext['user_agent'] = 'Mozilla/4.0 (MSIE 6.0; Windows NT 5.0)'; - } - - $context = stream_context_create(array( - 'http' => $httpContext, - 'ssl' => array( - 'verify_peer' => false, - 'verify_peer_name' => false, - 'allow_self_signed' => true, - ) - )); - } - - if ($context != null) { - $this->handle = @fopen($uri, 'rb', null, $context); - } else { - $this->handle = @fopen($uri, 'rb'); - } - } - - private function close() - { - if ($this->handle) { - if (is_resource($this->handle)) { - fclose($this->handle); - } - - $this->handle = null; - $this->type = null; - $this->str = null; - } - } - - private function getSize() - { - $this->pos = 0; - if (!$this->getType()) { - return array(0, 0); - } - - $size = $this->parseSize(); - if (!is_array($size) || count($size) < 2) { - return array(0, 0); - } - - return array_map('intval', array_values($size)); - } - - private function getType() - { - $this->pos = 0; - - if (!$this->type) { - switch ($this->getChars(2)) { - case 'BM': - return $this->type = 'bmp'; - case 'GI': - return $this->type = 'gif'; - case chr(0xFF) . chr(0xd8): - return $this->type = 'jpeg'; - case chr(0x89) . 'P': - return $this->type = 'png'; - case 'II': - case 'MM': - return $this->type = 'tiff'; - case 'RI': - return $this->type = 'webp'; - default: - return false; - } - } - - return $this->type; - } - - private function parseSize() - { - $this->pos = 0; - - $method = 'parseSizeFor' . strtoupper($this->type); - if (is_callable(array($this, $method))) { - return call_user_func(array($this, $method)); - } - - return null; - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForPNG() - { - $chars = $this->getChars(25); - - return unpack('N*', substr($chars, 16, 8)); - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForGIF() - { - $chars = $this->getChars(11); - - return unpack('S*', substr($chars, 6, 4)); - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForBMP() - { - $chars = $this->getChars(29); - $chars = substr($chars, 14, 14); - $type = unpack('C', $chars); - - return (reset($type) == 40) ? unpack('L*', substr($chars, 4)) : unpack('L*', substr($chars, 4, 8)); - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForJPEG() - { - $state = null; - $skip = 0; - - while (true) { - switch ($state) { - default: - $this->getChars(2); - $state = 'started'; - break; - - case 'started': - $b = $this->getByte(); - if ($b === false) { - return false; - } - - $state = $b == 0xFF ? 'sof' : 'started'; - break; - - case 'sof': - $b = $this->getByte(); - if (in_array($b, range(0xe0, 0xef), true)) { - $state = 'skipframe'; - } elseif (in_array($b, array_merge(range(0xC0, 0xC3), range(0xC5, 0xC7), - range(0xC9, 0xCB), range(0xCD, 0xCF)), true)) { - $state = 'readsize'; - } elseif ($b === 0xFF) { - $state = 'sof'; - } else { - $state = 'skipframe'; - } - break; - - case 'skipframe': - $skip = $this->readInt($this->getChars(2)) - 2; - $state = 'doskip'; - break; - - case 'doskip': - $this->getChars($skip); - $state = 'started'; - break; - - case 'readsize': - $c = $this->getChars(7); - - return array($this->readInt(substr($c, 5, 2)), $this->readInt(substr($c, 3, 2))); - } - } - - return array(0, 0); - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForWEBP() - { - $chars = $this->getChars(30); - $result = unpack('C12/S9', $chars); - - return array($result['8'], $result['9']); - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - private function parseSizeForTIFF() - { - $byteOrder = $this->getChars(2); - switch ($byteOrder) { - case 'II': - $short = 'v'; - $long = 'V'; - break; - case 'MM': - $short = 'n'; - $long = 'N'; - break; - default: - return false; - break; - } - - $this->getChars(2); - $offset = current(unpack($long, $this->getChars(4))); - - $this->getChars($offset - 8); - $tagCount = current(unpack($short, $this->getChars(2))); - - for ($i = $tagCount; $i > 0; $i--) { - $type = current(unpack($short, $this->getChars(2))); - $this->getChars(6); - $data = current(unpack($short, $this->getChars(2))); - switch ($type) { - case 0x0100: - $width = $data; - break; - case 0x0101: - $height = $data; - break; - case 0x0112: - $orientation = $data; - break; - } - if (isset($width) && isset($height) && isset($orientation)) { - if ($orientation >= 5) { - return array($height, $width); - } - return array($width, $height); - } - $this->getChars(2); - } - - return array(0, 0); - } - - private function getChars($n) - { - if (!is_resource($this->handle)) { - return false; - } - - $response = null; - - // do we need more data? - if ($this->pos + $n - 1 >= strlen($this->str)) { - $end = ($this->pos + $n); - - while (strlen($this->str) < $end && $response !== false) { - // read more from the file handle - $need = $end - ftell($this->handle); - - if ($response = fread($this->handle, $need)) { - $this->str .= $response; - } else { - return false; - } - } - } - - $result = substr($this->str, $this->pos, $n); - $this->pos += $n; - - if (function_exists('mb_convert_encoding')) { - $result = mb_convert_encoding($result, '8BIT', '7BIT'); - } - - return $result; - } - - private function getByte() - { - $c = $this->getChars(1); - if (!is_string($c) || strlen($c) === 0) { - return false; - } - - $b = unpack('C', $c); - - return reset($b); - } - - private function readInt($str) - { - $size = unpack('C*', $str); - if (!isset($size[1]) || !isset($size[2])) { - return 0; - } - - return ($size[1] << 8) + $size[2]; - } - - public function __destruct() - { - $this->close(); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/S3.php b/library/DevHelper/Helper/ShippableHelper/S3.php deleted file mode 100644 index 858f077..0000000 --- a/library/DevHelper/Helper/ShippableHelper/S3.php +++ /dev/null @@ -1,227 +0,0 @@ -_region = $region; - - switch ($region) { - case 'us-east-1': - // default endpoint is correct for this region - break; - default: - $this->setEndpoint(sprintf('http://s3-%s.amazonaws.com', $region)); - } - } - - public function setSignatureAws4($enabled) - { - $this->_signatureAws4 = $enabled; - } - - public function _makeRequest($method, $path = '', $params = null, $headers = array(), $data = null) - { - if (empty($params)) { - $params = array(); - } - - if (!is_array($headers)) { - $headers = array($headers); - } - - if (is_resource($data)) { - throw new Zend_Service_Amazon_S3_Exception("No support for stream data"); - } - $data = strval($data); - - // build the end point (with path) - $endpoint = clone($this->_endpoint); - $endpoint->setPath('/' . $path); - - $retryCount = 0; - - if ($this->_signatureAws4) { - if (isset($headers['Content-MD5'])) { - unset($headers['Content-MD5']); - } - - $headers['x-amz-content-sha256'] = Zend_Crypt::hash('sha256', $data); - $headers['x-amz-date'] = sprintf( - '%sT%sZ', - gmdate('Ymd', XenForo_Application::$time), - gmdate('His', XenForo_Application::$time) - ); - $headers['Host'] = parse_url($endpoint, PHP_URL_HOST); - - $this->addSignatureAws4($method, $path, $params, $headers); - } else { - $headers['Date'] = gmdate(DATE_RFC1123, time()); - self::addSignature($method, $path, $headers); - } - - $client = self::getHttpClient(); - - $client->resetParameters(); - $client->setUri($endpoint); - $client->setAuth(false); - $client->setHeaders($headers); - - if (is_array($params)) { - foreach ($params as $name => $value) { - $client->setParameterGet($name, $value); - } - } - - if (($method == 'PUT') && ($data !== null)) { - if (!isset($headers['Content-type'])) { - $headers['Content-type'] = self::getMimeType($path); - } - $client->setRawData($data, $headers['Content-type']); - } - - do { - $retry = false; - - $response = $client->request($method); - $responseCode = $response->getStatus(); - - if (XenForo_Application::debugMode() || $responseCode != 200) { - XenForo_Helper_File::log(__METHOD__, sprintf( - "%s %s %s -> %d %s\n\n", - $method, - $endpoint->getUri(), - var_export($headers, true), - $responseCode, - $responseCode != 200 - ? $response->getBody() - : sprintf('Body(length=%d)', strlen($response->getBody())) - )); - } - - // some 5xx errors are expected, so retry automatically - if ($responseCode >= 500 && $responseCode < 600 && $retryCount <= 5) { - $retry = true; - $retryCount++; - sleep($retryCount / 4 * $retryCount); - } elseif ($responseCode == 307) { - // need to redirect, new S3 endpoint given - // this should never happen as Zend_Http_Client will redirect automatically - } elseif ($responseCode == 100) { - // 'OK to Continue'; - } - } while ($retry); - - return $response; - } - - protected function addSignatureAws4($method, $path, array $params, array &$headers) - { - // http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html - // task 1: create a canonical request - $canonicalQueryArray = array(); - if (!empty($params)) { - ksort($params); - foreach ($params as $paramKey => $paramValue) { - $canonicalQueryArray[] = sprintf('%s=%s', urlencode($paramKey), urlencode($paramValue)); - } - } - $canonicalQueryString = implode('&', $canonicalQueryArray); - - $canonicalHeaders = ''; - $signedHeadersArray = array(); - $hashedPayload = ''; - $timestamp = ''; - $headerKeys = array_combine(array_map('strtolower', array_keys($headers)), array_keys($headers)); - ksort($headerKeys); - foreach ($headerKeys as $headerKeyLower => $headerKey) { - $canonicalHeaders .= sprintf("%s:%s\n", $headerKeyLower, $headers[$headerKey]); - $signedHeadersArray[] = $headerKeyLower; - - switch ($headerKey) { - case 'x-amz-content-sha256': - $hashedPayload = $headers[$headerKey]; - break; - case 'x-amz-date': - $timestamp = $headers[$headerKey]; - break; - } - } - $signedHeadersString = implode(';', $signedHeadersArray); - - $canonicalRequest = sprintf( - "%s\n/%s\n%s\n%s\n%s\n%s", - $method, - $path, - $canonicalQueryString, - $canonicalHeaders, - $signedHeadersString, - $hashedPayload - ); - - // task 2: create a string to sign - $date = substr($timestamp, 0, strpos($timestamp, 'T')); - $scope = sprintf( - '%s/%s/s3/aws4_request', - $date, - $this->_region - ); - $stringToSign = sprintf( - "AWS4-HMAC-SHA256\n%s\n%s\n%s", - $timestamp, - $scope, - Zend_Crypt::hash('sha256', $canonicalRequest) - ); - - // task 3: calculate signature - $dateKey = Zend_Crypt_Hmac::compute('AWS4' . $this->_getSecretKey(), 'sha256', - $date, Zend_Crypt_Hmac::BINARY); - $dateRegionKey = Zend_Crypt_Hmac::compute($dateKey, - 'sha256', $this->_region, Zend_Crypt_Hmac::BINARY); - $dateRegionServiceKey = Zend_Crypt_Hmac::compute($dateRegionKey, - 'sha256', 's3', Zend_Crypt_Hmac::BINARY); - $signingKey = Zend_Crypt_Hmac::compute($dateRegionServiceKey, - 'sha256', 'aws4_request', Zend_Crypt_Hmac::BINARY); - - $signature = Zend_Crypt_Hmac::compute($signingKey, 'sha256', $stringToSign); - $headers['Authorization'] = sprintf( - 'AWS4-HMAC-SHA256 Credential=%s/%s,SignedHeaders=%s,Signature=%s', - $this->_getAccessKey(), - $scope, - $signedHeadersString, - $signature - ); - - return $signature; - } - - public static function getRegions() - { - // http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region - return array( - 'us-east-1' => 'US East (N. Virginia)', - 'us-east-2' => 'US East (Ohio)', - 'us-west-1' => 'US West (N. California)', - 'us-west-2' => 'US West (Oregon)', - 'ca-central-1' => 'Canada (Central)', - 'ap-south-1' => 'Asia Pacific (Mumbai)', - 'ap-northeast-2' => 'Asia Pacific (Seoul)', - 'ap-southeast-1' => 'Asia Pacific (Singapore)', - 'ap-southeast-2' => 'Asia Pacific (Sydney)', - 'ap-northeast-1' => 'Asia Pacific (Tokyo)', - 'eu-central-1' => 'EU (Frankfurt)', - 'eu-west-1' => 'EU (Ireland)', - 'eu-west-2' => 'EU (London)', - 'sa-east-1' => 'South America (São Paulo)', - ); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/String.php b/library/DevHelper/Helper/ShippableHelper/String.php deleted file mode 100644 index 730a7c2..0000000 --- a/library/DevHelper/Helper/ShippableHelper/String.php +++ /dev/null @@ -1,26 +0,0 @@ - 1024 && $unitKey < count($units) - 1) { - $bytes /= 1024; - $unitKey++; - } - - // get float with one decimal place - // truncate if it's insignificant though - $value = sprintf('%.1f', $bytes); - $value = preg_replace('#\.0$#', '', $value); - - return sprintf('%s %s', $value, $units[$unitKey]); - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/TempFile.php b/library/DevHelper/Helper/ShippableHelper/TempFile.php deleted file mode 100644 index e82e72b..0000000 --- a/library/DevHelper/Helper/ShippableHelper/TempFile.php +++ /dev/null @@ -1,213 +0,0 @@ - '', - 'userAgent' => '', - 'timeOutInSeconds' => 0, - 'maxRedirect' => 3, - 'maxDownloadSize' => XenForo_Application::getOptions()->get('attachmentMaxFileSize') * 1024, - 'secured' => 0, - ); - - $tempFile = trim(strval($options['tempFile'])); - $managedTempFile = false; - if (strlen($tempFile) === 0) { - $tempFile = tempnam(XenForo_Helper_File::getTempDir(), self::_getPrefix()); - $managedTempFile = true; - } - if (strlen($tempFile) === 0) { - return false; - } - - if (isset(self::$_cached[$url]) - && filesize(self::$_cached[$url]) > 0 - ) { - if ($managedTempFile) { - unlink($tempFile); - return self::$_cached[$url]; - } else { - copy(self::$_cached[$url], $tempFile); - return $tempFile; - } - } - - if ($managedTempFile) { - self::cache($url, $tempFile); - } - - self::$_maxDownloadSize = $options['maxDownloadSize']; - - $fh = fopen($tempFile, 'wb'); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_FILE, $fh); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_NOPROGRESS, 0); - curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, array(__CLASS__, 'download_curlProgressFunction')); - - if (!empty($options['userAgent'])) { - curl_setopt($ch, CURLOPT_USERAGENT, $options['userAgent']); - } - if ($options['timeOutInSeconds'] > 0) { - curl_setopt($ch, CURLOPT_TIMEOUT, $options['timeOutInSeconds']); - } - if ($options['maxRedirect'] > 0) { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_MAXREDIRS, $options['maxRedirect']); - } else { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); - } - if ($options['secured'] === 0) { - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - } - - curl_exec($ch); - $curlInfo = curl_getinfo($ch); - curl_close($ch); - fclose($fh); - - $downloaded = true; - if (!isset($curlInfo['http_code']) - || $curlInfo['http_code'] < 200 - || $curlInfo['http_code'] >= 300 - ) { - // no http response status / non success status, must be an error - $downloaded = false; - } - - $fileSize = filesize($tempFile); - if ($downloaded && $fileSize === 0) { - clearstatcache(); - $fileSize = filesize($tempFile); - } - - if ($downloaded - && isset($curlInfo['size_download']) - && $fileSize !== intval($curlInfo['size_download']) - ) { - // file size reported by our system seems to be off, probably a write error - $downloaded = false; - } - - if ($downloaded - && isset($curlInfo['download_content_length']) - && $curlInfo['download_content_length'] > 0 - && $fileSize !== intval($curlInfo['download_content_length']) - ) { - // file size is different from Content-Length header, probably a cancelled download (or corrupted) - $downloaded = false; - } - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, call_user_func_array('sprintf', array( - 'download %s -> %s, %s, %s', - $url, - $tempFile, - ($downloaded ? 'succeeded' : 'failed'), - json_encode($curlInfo), - ))); - } - - if ($downloaded) { - return $tempFile; - } else { - file_put_contents($tempFile, ''); - return false; - } - } - - public static function download_curlProgressFunction($downloadSize, $downloaded) - { - return ((self::$_maxDownloadSize > 0 - && ($downloadSize > self::$_maxDownloadSize - || $downloaded > self::$_maxDownloadSize)) - ? 1 : 0); - } - - public static function deleteAllCached() - { - foreach (self::$_cached as $url => $tempFile) { - if (XenForo_Application::debugMode()) { - $fileSize = @filesize($tempFile); - } - - $deleted = @unlink($tempFile); - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, call_user_func_array('sprintf', array( - 'delete %s -> %s, %s, %d bytes', - $url, - $tempFile, - ($deleted ? 'succeeded' : 'failed'), - (!empty($fileSize) ? $fileSize : 0), - ))); - } - } - - self::$_cached = array(); - } - - protected static function _getPrefix() - { - static $prefix = null; - - if ($prefix === null) { - $prefix = strtolower(preg_replace('#[^A-Z]#', '', __CLASS__)) . '_'; - } - - return $prefix; - } - -} diff --git a/library/DevHelper/Helper/ShippableHelper/Updater.php b/library/DevHelper/Helper/ShippableHelper/Updater.php deleted file mode 100644 index 6a0e0c4..0000000 --- a/library/DevHelper/Helper/ShippableHelper/Updater.php +++ /dev/null @@ -1,195 +0,0 @@ - array(), - 'onPreRoute' => array(), - 'setupMethod' => array(), - 'addOns' => array(), - ); - } - - $GLOBALS[self::KEY]['version'][$apiUrl][self::$_version] - = 'DevHelper_Helper_ShippableHelper_UpdaterCore'; - - if (!isset($GLOBALS[self::KEY]['onPreRoute'][$apiUrl])) { - $GLOBALS[self::KEY]['onPreRoute'][$apiUrl] = create_function('$fc', - __CLASS__ . '::onPreRoute($fc, ' . var_export($config, true) . ');'); - XenForo_CodeEvent::addListener('front_controller_pre_route', - $GLOBALS[self::KEY]['onPreRoute'][$apiUrl]); - } - - $GLOBALS[self::KEY][$apiUrl]['addOns'][$addOnId] = true; - } - - /** - * Removes trace of Updater if it is no longer needed. - * - * @param string|null $apiUrl - * @param string|null $addOnId - * @throws Zend_Exception - */ - public static function onUninstall($apiUrl = null, $addOnId = null) - { - if (XenForo_Application::$versionId < 1020000) { - // only run if XenForo is at least 1.2.0 - return; - } - - if (!is_string($apiUrl)) { - // use default api url if nothing provided - $apiUrl = self::API_URL; - } - - $addOnId = self::_getAddOnId($addOnId); - $config = self::_readConfig($addOnId, $apiUrl); - $addOns = XenForo_Application::get('addOns'); - - $activeAddOnIds = array(); - foreach ($config['addOnIds'] as $configAddOnId) { - if ($configAddOnId === $addOnId - || !isset($addOns[$configAddOnId]) - ) { - continue; - } - - $activeAddOnIds[] = $configAddOnId; - } - - if (empty($activeAddOnIds)) { - // no active add-ons found, trigger uninstall routine - DevHelper_Helper_ShippableHelper_UpdaterCore::_uninstallSelf($apiUrl); - } - } - - /** - * Finds the latest Updater version and let it setup. - * THIS METHOD SHOULD NOT BE ALTERED BETWEEN VERSIONS. - * - * @param XenForo_FrontController $fc - * @param array $config - * - * @internal - */ - public static function onPreRoute( - /** @noinspection PhpUnusedParameterInspection */ - XenForo_FrontController $fc, - array $config - ) { - if (empty($GLOBALS[self::KEY]['version'][$config['apiUrl']])) { - return; - } - $versions = $GLOBALS[self::KEY]['version'][$config['apiUrl']]; - - ksort($versions); - $latest = array_pop($versions); - - call_user_func(array($latest, 'bootstrap'), $config); - } - - public static function getConfigOptionId($apiUrl) - { - $configOptionId = sprintf('updater%s%s', str_replace(' ', '', ucwords(preg_replace('#[^A-Za-z]+#', ' ', - parse_url($apiUrl, PHP_URL_HOST)))), md5($apiUrl)); - if (strlen($configOptionId) > 50) { - $configOptionId = 'updater_' . md5($configOptionId); - } - - return $configOptionId; - } - - private static function _getAddOnId($addOnId) - { - if ($addOnId !== null) { - return $addOnId; - } - - $clazz = __CLASS__; - $strPos = strpos($clazz, '_ShippableHelper_'); - if ($strPos === false) { - throw new XenForo_Exception('Unable to determine $addOnId'); - } - - return substr($clazz, 0, $strPos); - } - - private static function _readConfig($addOnId, $apiUrl) - { - $options = XenForo_Application::getOptions(); - - $configOptionId = self::getConfigOptionId($apiUrl); - $config = $options->get($configOptionId); - - if ($config === null) { - $config = array( - 'version' => 0, - 'apiUrl' => $apiUrl, - 'addOnIds' => array($addOnId), - - 'enabled' => false, - 'ignored' => array(), - ); - } - - return $config; - } -} \ No newline at end of file diff --git a/library/DevHelper/Helper/ShippableHelper/UpdaterCore.php b/library/DevHelper/Helper/ShippableHelper/UpdaterCore.php deleted file mode 100644 index 586771d..0000000 --- a/library/DevHelper/Helper/ShippableHelper/UpdaterCore.php +++ /dev/null @@ -1,794 +0,0 @@ -$config.apiUrlviewName) { - case 'XenForo_ViewAdmin_AddOn_List': - self::_onAddOnList($controllerResponse, $containerParams); - break; - case 'XenForo_ViewAdmin_AddOn_Upgrade': - self::_onAddOnUpgrade($controllerResponse); - break; - } - } - - private static function _onAddOnList(XenForo_ControllerResponse_View &$controllerResponse, array &$containerParams) - { - $message = ''; - - if (!empty($_GET[self::PARAM_ENABLE]) - && $_GET[self::PARAM_ENABLE] === self::$_config['apiUrl'] - ) { - self::$_config['configured'] = true; - self::$_config['enabled'] = true; - self::_saveConfig(self::$_config); - $controllerResponse = new XenForo_ControllerResponse_Redirect(); - $controllerResponse->redirectType = XenForo_ControllerResponse_Redirect::RESOURCE_UPDATED; - $controllerResponse->redirectTarget = XenForo_Link::buildAdminLink('add-ons'); - return; - } - - if (!empty($_GET[self::PARAM_DISABLE]) - && $_GET[self::PARAM_DISABLE] === self::$_config['apiUrl'] - ) { - self::$_config['configured'] = true; - self::$_config['enabled'] = false; - self::_saveConfig(self::$_config); - $controllerResponse = new XenForo_ControllerResponse_Redirect(); - $controllerResponse->redirectType = XenForo_ControllerResponse_Redirect::RESOURCE_UPDATED; - $controllerResponse->redirectTarget = XenForo_Link::buildAdminLink('add-ons'); - return; - } - - if (empty(self::$_config['configured'])) { - if (!self::$_isBeta - || XenForo_Application::debugMode() - ) { - $enabledAddOnIds = self::_getEnabledAddOnIds(self::$_config['addOnIds']); - - if (!empty($enabledAddOnIds)) { - /** @noinspection HtmlUnknownTarget */ - $message .= sprintf('Updater for %s: enable or disable?', - implode(', ', $enabledAddOnIds), - XenForo_Link::buildAdminLink('add-ons', null, array( - self::PARAM_ENABLE => self::$_config['apiUrl'], - )), - XenForo_Link::buildAdminLink('add-ons', null, array( - self::PARAM_DISABLE => self::$_config['apiUrl'], - )) - ); - - if (self::$_isBeta) { - $message .= ' Warning: The Updater is currently in beta.'; - } - } - } - } elseif (self::$_config['configured']) { - $forceRefresh = (!empty($_GET[self::PARAM_FORCE]) && $_GET[self::PARAM_FORCE] === self::$_config['apiUrl']); - $data = self::_refreshData(self::$_config['apiUrl'], self::$_config['addOnIds'], $forceRefresh); - - if (!empty($data)) { - $message .= self::_onAddOnShowStatuses($data, $forceRefresh); - } - - } - - $paramKey = '_' . md5(self::$_config['apiUrl']); - $containerParams[$paramKey] = array('message' => $message); - } - - private static function _onAddOnShowStatuses(array $data, $forceRefresh) - { - $statuses = ''; - $xenAddOns = XenForo_Application::get('addOns'); - - foreach ($xenAddOns as $addOnId => $addOnVersionId) { - if (!in_array($addOnId, self::$_config['addOnIds'], true)) { - continue; - } - - if (!isset($data[$addOnId])) { - if ($forceRefresh) { - $statuses .= sprintf('%1$s cannot be verified with the Updater Server.
', $addOnId); - } - continue; - } - - if ($addOnVersionId < $data[$addOnId]['version_id']) { - $ignoredVersionId = 0; - if (!empty(self::$_config['ignored'][$addOnId])) { - $ignoredVersionId = self::$_config['ignored'][$addOnId]; - } - $isIgnored = $data[$addOnId]['version_id'] <= $ignoredVersionId; - - $actions = array(); - if ($forceRefresh - || !$isIgnored - ) { - if (!empty($data[$addOnId]['links']['permalink'])) { - /** @noinspection HtmlUnknownTarget */ - $actions[] = sprintf('read more', $data[$addOnId]['links']['permalink']); - } - - if (!empty($data[$addOnId]['links']['content'])) { - /** @noinspection HtmlUnknownTarget */ - $actions[] = sprintf('update', - XenForo_Link::buildAdminLink('full:add-ons/upgrade', - array('addon_id' => $addOnId), - array(self::PARAM_DOWNLOAD => self::$_config['apiUrl']))); - } elseif (!empty($data[$addOnId]['links']['authorize'])) { - $authorizeUrl = sprintf('%s&redirect_uri=%s', - $data[$addOnId]['links']['authorize'], - rawurlencode(XenForo_Link::buildAdminLink('full:add-ons/upgrade', - array('addon_id' => $addOnId), - array(self::PARAM_AUTHORIZE => self::$_config['apiUrl']))) - ); - - /** @noinspection HtmlUnknownTarget */ - $actions[] = sprintf('authorize an update', $authorizeUrl); - } - } - - if (!empty($actions)) { - $outOfDateMessage = sprintf('%1$s is out of date, latest version is v%3$s (#%4$d, yours is #%2$d).', - $addOnId, - $addOnVersionId, - $data[$addOnId]['version_string'], - $data[$addOnId]['version_id']); - - $ignoreMessage = ''; - if (!$isIgnored) { - /** @noinspection HtmlUnknownTarget */ - $ignoreMessage = sprintf(' or ignore this version', - XenForo_Link::buildAdminLink('full:add-ons/upgrade', - array('addon_id' => $addOnId), - array(self::PARAM_IGNORE => self::$_config['apiUrl']))); - } - - $statuses .= sprintf('%1$s You may: %2$s%3$s.
', - $outOfDateMessage, - implode(', ', $actions), - $ignoreMessage); - } - } else { - if ($forceRefresh) { - if ($addOnVersionId > $data[$addOnId]['version_id']) { - $statuses .= sprintf('%1$s appears to be even newer than' - . ' reported from the Updater Server: their v%3$s (#%4$d) vs. your #%2$d.' - . ' Awesome!
', - $addOnId, - $addOnVersionId, - $data[$addOnId]['version_string'], - $data[$addOnId]['version_id']); - } else { - $statuses .= sprintf('%1$s is up to date.
', $addOnId); - } - } - } - } - - return $statuses; - } - - private static function _onAddOnUpgrade(XenForo_ControllerResponse_View &$controllerResponse) - { - if (!empty($_GET[self::PARAM_AUTHORIZE]) - && $_GET[self::PARAM_AUTHORIZE] === self::$_config['apiUrl'] - ) { - $downloadLink = XenForo_Link::buildAdminLink( - 'full:add-ons/upgrade', - $controllerResponse->params['addOn'], - array( - self::PARAM_DOWNLOAD => self::$_config['apiUrl'] - ) - ); - $downloadLinkJson = json_encode($downloadLink); - $accessTokenParamJson = json_encode(self::PARAM_ACCESS_TOKEN); - - $js = << -var hash = window.location.hash.substr(1); -var regex = /access_token=(.+?)(&|$)/; -var match = hash.match(regex); -if (match) { - var accessToken = match[1]; - var downloadLink = $downloadLinkJson; - window.location = downloadLink + '&' + $accessTokenParamJson + '=' + encodeURIComponent(accessToken); -} - -EOF; - die($js); - } - - if (!empty($_GET[self::PARAM_DOWNLOAD]) - && $_GET[self::PARAM_DOWNLOAD] === self::$_config['apiUrl'] - ) { - $accessToken = ''; - if (!empty($_GET[self::PARAM_ACCESS_TOKEN])) { - $accessToken = $_GET[self::PARAM_ACCESS_TOKEN]; - } - - $addOnId = $controllerResponse->params['addOn']['addon_id']; - $data = self::_fetchData(self::$_config['apiUrl'], array($addOnId), $accessToken); - - if (!empty($data[$addOnId]['links']['content'])) { - $tempFile = DevHelper_Helper_ShippableHelper_TempFile::download($data[$addOnId]['links']['content']); - $xmlPath = self::_updateAddOnFiles($addOnId, $tempFile); - - /** @noinspection BadExpressionStatementJS */ - die(sprintf('', - json_encode(XenForo_Link::buildAdminLink('add-ons/upgrade', array('addon_id' => $addOnId), array( - self::PARAM_IMPORT_XML => self::$_config['apiUrl'], - self::PARAM_IMPORT_XML_PATH => $xmlPath - )))) - ); - } - } - - if (!empty($_GET[self::PARAM_IMPORT_XML]) - && $_GET[self::PARAM_IMPORT_XML] === self::$_config['apiUrl'] - && !empty($_GET[self::PARAM_IMPORT_XML_PATH]) - ) { - $addOnId = $controllerResponse->params['addOn']['addon_id']; - $xmlPath = $_GET[self::PARAM_IMPORT_XML_PATH]; - - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = XenForo_Model::create('XenForo_Model_AddOn'); - $addOnModel->installAddOnXmlFromFile($xmlPath, $addOnId); - - $addOn = $addOnModel->getAddOnById($addOnId); - echo(sprintf('Updated add-on %1$s to v%2$s (#%3$d)', - $addOn['title'], $addOn['version_string'], $addOn['version_id'])); - - /** @noinspection BadExpressionStatementJS */ - die(sprintf('', - json_encode(XenForo_Link::buildAdminLink('full:tools/run-deferred', null, array( - 'redirect' => XenForo_Link::buildAdminLink('full:add-ons') - ))))); - } - - if (!empty($_GET[self::PARAM_IGNORE]) - && $_GET[self::PARAM_IGNORE] === self::$_config['apiUrl'] - ) { - $addOnId = $controllerResponse->params['addOn']['addon_id']; - $data = self::_refreshData(self::$_config['apiUrl'], self::$_config['addOnIds'], false); - if (!empty($data[$addOnId]['version_id'])) { - self::$_config['ignored'][$addOnId] = $data[$addOnId]['version_id']; - self::_saveConfig(self::$_config); - } - - $controllerResponse = new XenForo_ControllerResponse_Redirect(); - $controllerResponse->redirectType = XenForo_ControllerResponse_Redirect::RESOURCE_UPDATED; - $controllerResponse->redirectTarget = XenForo_Link::buildAdminLink('add-ons'); - } - } - - private static function _updateSelf(array $config) - { - $options = XenForo_Application::getOptions(); - - $configOptionId = DevHelper_Helper_ShippableHelper_Updater::getConfigOptionId($config['apiUrl']); - $templateTitle = $configOptionId; - $templateModKey = $configOptionId; - - $configVersion = $config['version']; - $configUpdated = array(); - if ($configVersion === 0) { - $configUpdated[] = '$config'; - } - - if ($configVersion < DevHelper_Helper_ShippableHelper_Updater::$_version) { - if ($configVersion < 2015100121) { - /** @var XenForo_DataWriter_AdminTemplate $templateDw */ - $templateDw = XenForo_DataWriter::create('XenForo_DataWriter_AdminTemplate'); - - /** @var XenForo_Model_AdminTemplate $templateModel */ - $templateModel = XenForo_Model::create('XenForo_Model_AdminTemplate'); - $template = $templateModel->getAdminTemplateByTitle($templateTitle); - if (!empty($template)) { - $templateDw->setExistingData($template, true); - } - - $templateDw->bulkSet(array( - 'title' => $templateTitle, - 'template' => self::$_template, - )); - $templateDw->save(); - - $configUpdated[] = '$templateDw'; - } - - if ($configVersion < 2015100107) { - /** @var XenForo_DataWriter_AdminTemplateModification $templateModDw */ - $templateModDw = XenForo_DataWriter::create('XenForo_DataWriter_AdminTemplateModification'); - - /** @var XenForo_Model_AdminTemplateModification $templateModModel */ - $templateModModel = XenForo_Model::create('XenForo_Model_AdminTemplateModification'); - $templateMod = $templateModModel->getModificationByKey($templateModKey); - if (!empty($templateMod)) { - $templateModDw->setExistingData($templateMod, true); - } - - $templateModDw->bulkSet(array( - 'template' => 'content_header', - 'modification_key' => $templateModKey, - 'description' => $config['apiUrl'], - 'action' => 'preg_replace', - 'find' => '#^.+$#s', - 'replace' => '$0' . str_replace('md5', md5($config['apiUrl']), self::$_templateMod), - )); - $templateModDw->save(); - - $configUpdated[] = '$templateModDw'; - } - - $config['version'] = DevHelper_Helper_ShippableHelper_Updater::$_version; - $configUpdated[] = '$config["version"]'; - } - - $addOnIdsUpdated = false; - foreach ($GLOBALS[DevHelper_Helper_ShippableHelper_Updater::KEY][$config['apiUrl']]['addOns'] as $addOnId => $enabled) { - if (!in_array($addOnId, $config['addOnIds'], true)) { - $config['addOnIds'][] = $addOnId; - $addOnIdsUpdated = true; - } - } - if ($addOnIdsUpdated) { - sort($config['addOnIds']); - $configUpdated[] = '$config["addOnIds"]'; - } - - if (count($configUpdated) > 0) { - /** @var XenForo_DataWriter_Option $optionDw */ - $optionDw = XenForo_DataWriter::create('XenForo_DataWriter_Option'); - if ($configVersion > 0) { - $optionDw->setExistingData($configOptionId); - } - $optionDw->bulkSet(array( - 'option_id' => $configOptionId, - 'option_value' => $config, - 'default_value' => 'a:0:{}', - 'edit_format' => 'template', - 'edit_format_params' => $templateTitle, - 'data_type' => 'array', - 'sub_options' => '*', - )); - $optionDw->setRelations(array( - 'debug' => 9999, - )); - $optionDw->setExtraData(XenForo_DataWriter_Option::DATA_TITLE, 'Add-on Updates' - . (self::$_isBeta ? ' (BETA)' : '')); - $optionDw->setExtraData(XenForo_DataWriter_Option::DATA_EXPLAIN, 'Turn on this option if you want to' - . ' check for add-on updates from ' - . $config['apiUrl'] . ' regularly. If you want to perform update check immediately, click here.'); - $optionDw->save(); - $options->set($configOptionId, $config); - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($apiUrl=%s): $configVersion=%d,' - . ' self::$_version=%d, $configUpdated=%s', - __METHOD__, $config['apiUrl'], var_export($configVersion, true), - var_export(DevHelper_Helper_ShippableHelper_Updater::$_version, true), - var_export($configUpdated, true))); - } - } - - self::$_config = $config; - } - - private static function _updateAddOnFiles($addOnId, $tempFile) - { - $tempDir = $tempFile . '_extracted'; - - XenForo_Helper_File::createDirectory($tempDir, false); - XenForo_Helper_File::makeWritableByFtpUser($tempDir); - - $decompress = new Zend_Filter_Decompress(array( - 'adapter' => 'Zip', - 'options' => array('target' => $tempDir) - )); - - if (!$decompress->filter($tempFile)) { - throw new XenForo_Exception('Unable to extract add-on package.', true); - } - - $uploadDir = sprintf('%s/upload', $tempDir); - if (!is_dir($uploadDir)) { - throw new XenForo_Exception('Unsupported add-on package (no "upload" directory found).', true); - } - - $xfDir = dirname(XenForo_Autoloader::getInstance()->getRootDir()); - $files = self::_verifyAddOnFiles($uploadDir, $xfDir); - - $xmlPath = self::_findXmlPath($addOnId, $tempDir, $xfDir, $files); - - self::_moveAddOnFiles($uploadDir, $xfDir, $files); - - return $xmlPath; - } - - private static function _verifyAddOnFiles($packageDir, $xfDir, $relativePath = null) - { - $files = array(); - - $dir = sprintf('%s/%s', $packageDir, $relativePath); - $entries = scandir($dir); - $subDirs = array(); - - foreach ($entries as $entry) { - if (substr($entry, 0, 1) === '.') { - // ignore hidden files - continue; - } - - if ($relativePath !== null) { - $fileRelativePath = sprintf('%s/%s', $relativePath, $entry); - } else { - $fileRelativePath = $entry; - } - - if (is_file(sprintf('%s/%s', $packageDir, $fileRelativePath))) { - $files[] = $fileRelativePath; - - $fileSystemPath = sprintf('%s/%s', $xfDir, $fileRelativePath); - if (is_file($fileSystemPath)) { - if (!is_writable($fileSystemPath)) { - throw new XenForo_Exception('File is not writable: ' . $fileSystemPath, true); - } - } elseif (is_dir($fileSystemPath)) { - throw new XenForo_Exception('File/directory conflict: ' . $fileSystemPath, true); - } else { - $parentOfFileSystemPath = dirname($fileSystemPath); - if (is_dir($parentOfFileSystemPath)) { - if (!is_writable($parentOfFileSystemPath)) { - throw new XenForo_Exception('Directory is not writable: ' - . $parentOfFileSystemPath, true); - } - } else { - if (!XenForo_Helper_File::createDirectory($parentOfFileSystemPath)) { - throw new XenForo_Exception('Directory cannot be created: ' - . $parentOfFileSystemPath, true); - } - } - } - } else { - $subDirs[] = $fileRelativePath; - } - } - - foreach ($subDirs as $subDir) { - $files = array_merge($files, self::_verifyAddOnFiles($packageDir, $xfDir, $subDir)); - } - - return $files; - } - - private static function _moveAddOnFiles($packageDir, $xfDir, array $files) - { - foreach ($files as $file) { - $packagePath = sprintf('%s/%s', $packageDir, $file); - $systemPath = sprintf('%s/%s', $xfDir, $file); - - if (!XenForo_Helper_File::safeRename($packagePath, $systemPath)) { - throw new XenForo_Exception('File cannot be updated: ' . $file); - } - } - } - - private static function _findXmlPath($addOnId, $tempDir, $xfDir, array $files) - { - foreach ($files as $file) { - $fileName = basename($file); - if (preg_match('#^addon-(?.+)\.xml$#', $fileName, $matches)) { - if ($addOnId === $matches['id']) { - return sprintf('%s/%s', $xfDir, $file); - } - } - } - - $entries = scandir($tempDir); - foreach ($entries as $entry) { - if (preg_match('#^addon-(?.+)\.xml$#', $entry, $matches)) { - if ($addOnId === $matches['id']) { - return sprintf('%s/%s', $tempDir, $entry); - } - } - } - - throw new XenForo_Exception('Unsupported add-on package (no xml found).', true); - } - - public static function _uninstallSelf($apiUrl) - { - $configOptionId = DevHelper_Helper_ShippableHelper_Updater::getConfigOptionId($apiUrl); - $templateTitle = $configOptionId; - $templateModKey = $configOptionId; - - try { - XenForo_Db::beginTransaction(); - - /** @var XenForo_Model_AdminTemplate $templateModel */ - $templateModel = XenForo_Model::create('XenForo_Model_AdminTemplate'); - $template = $templateModel->getAdminTemplateByTitle($templateTitle); - if (!empty($template)) { - /** @var XenForo_DataWriter_AdminTemplate $templateDw */ - $templateDw = XenForo_DataWriter::create('XenForo_DataWriter_AdminTemplate'); - $templateDw->setExistingData($template, true); - $templateDw->delete(); - } - - - /** @var XenForo_Model_AdminTemplateModification $templateModModel */ - $templateModModel = XenForo_Model::create('XenForo_Model_AdminTemplateModification'); - $templateMod = $templateModModel->getModificationByKey($templateModKey); - if (!empty($templateMod)) { - /** @var XenForo_DataWriter_AdminTemplateModification $templateModDw */ - $templateModDw = XenForo_DataWriter::create('XenForo_DataWriter_AdminTemplateModification'); - $templateModDw->setExistingData($templateMod, true); - $templateModDw->delete(); - } - - /** @var XenForo_DataWriter_Option $optionDw */ - $optionDw = XenForo_DataWriter::create('XenForo_DataWriter_Option'); - $optionDw->setExistingData($configOptionId); - $optionDw->delete(); - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($apiUrl=%s): ok', - __METHOD__, $apiUrl)); - } - - XenForo_Db::commit(); - } catch (XenForo_Exception $e) { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($apiUrl=%s): %s', - __METHOD__, $apiUrl, $e->__toString())); - } - - XenForo_Db::rollback(); - } - } - - private static function _refreshData($apiUrl, array $addOnIds, $forceRefresh) - { - $data = ($forceRefresh ? null : self::_getCache($apiUrl)); - if (empty($data) - || $data['version'] < DevHelper_Helper_ShippableHelper_Updater::$_version - || $data['timestamp'] < XenForo_Application::$time - 86400 - ) { - $data = array( - 'version' => DevHelper_Helper_ShippableHelper_Updater::$_version, - 'timestamp' => time(), - 'json' => self::_fetchData($apiUrl, $addOnIds), - ); - - self::_setCache($apiUrl, $data); - } - - return $data['json']; - } - - private static function _fetchData($url, array $addOnIds, $accessToken = '') - { - if (empty($addOnIds)) { - return array(); - } - - $url .= sprintf('%s_version=%d', ((strpos($url, '?') === false) ? '?' : '&'), - DevHelper_Helper_ShippableHelper_Updater::$_version); - if (!empty($accessToken)) { - $url .= sprintf('&oauth_token=%s', rawurlencode($accessToken)); - } - foreach ($addOnIds as $addOnId) { - $url .= sprintf('&ids[]=%s', rawurlencode($addOnId)); - } - - $client = XenForo_Helper_Http::getClient($url); - - try { - $response = $client->request('GET'); - - $responseStatus = $response->getStatus(); - $responseBody = $response->getBody(); - } catch (Exception $e) { - $responseStatus = 503; - $responseBody = $e->getMessage(); - } - - $json = null; - if ($responseStatus === 200) { - $json = @json_decode($responseBody, true); - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($url=%s): status=%d, json=%s', - __METHOD__, $url, - var_export($responseStatus, true), var_export($json, true))); - } - } else { - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($url=%s): status=%d, body=%s', - __METHOD__, $url, - var_export($responseStatus, true), var_export($responseBody, true))); - } - } - - return $json; - } - - private static function _getCache($apiUrl) - { - /** @var XenForo_Model_DataRegistry $dataRegistryModel */ - $dataRegistryModel = XenForo_Model::create('XenForo_Model_DataRegistry'); - $data = $dataRegistryModel->get(DevHelper_Helper_ShippableHelper_Updater::KEY); - - if (!empty($data) - && isset($data[$apiUrl]) - ) { - return $data[$apiUrl]; - } - - return array(); - } - - private static function _setCache($apiUrl, array $thisData) - { - /** @var XenForo_Model_DataRegistry $dataRegistryModel */ - $dataRegistryModel = XenForo_Model::create('XenForo_Model_DataRegistry'); - $data = $dataRegistryModel->get(DevHelper_Helper_ShippableHelper_Updater::KEY); - if (empty($data)) { - $data = array(); - } - $data[$apiUrl] = $thisData; - $dataRegistryModel->set(DevHelper_Helper_ShippableHelper_Updater::KEY, $data); - return true; - } - - private static function _saveConfig(array $config) - { - $configOptionId = DevHelper_Helper_ShippableHelper_Updater::getConfigOptionId($config['apiUrl']); - - /** @var XenForo_DataWriter_Option $optionDw */ - $optionDw = XenForo_DataWriter::create('XenForo_DataWriter_Option'); - $optionDw->setExistingData($configOptionId); - $optionDw->set('option_value', $config); - - if ($optionDw->save()) { - XenForo_Application::getOptions()->set($configOptionId, $config); - - if (XenForo_Application::debugMode()) { - XenForo_Helper_File::log(__CLASS__, sprintf('%s($apiUrl=%s)', - __METHOD__, $config['apiUrl'])); - } - - return true; - } - - return false; - } - - private static function _getEnabledAddOnIds($addOnIds) - { - $xenAddOns = XenForo_Application::get('addOns'); - $enabledIds = array(); - foreach ($addOnIds as $configAddOnId) { - if (isset($xenAddOns[$configAddOnId])) { - $enabledIds[] = $configAddOnId; - } - } - - return $enabledIds; - } - - private static $_template = ' - - - - - {$configAddOnId} - - - - - - - - {xen:raw $preparedOption.explain} - Supported add-ons: {$addOnListStr} - - - - {xen:raw $editLink} - - - - - - - - -'; - - private static $_templateMod = ' - -

{xen:raw $_md5.message}

-
'; - - private static $_config = null; -} \ No newline at end of file diff --git a/library/DevHelper/Helper/Template.php b/library/DevHelper/Helper/Template.php deleted file mode 100644 index a6ea331..0000000 --- a/library/DevHelper/Helper/Template.php +++ /dev/null @@ -1,56 +0,0 @@ -getClassPrefix() . '_'; - $prefixLength = strlen($prefix); - - foreach (self::$_foundXfcpClasses as $clazz => $paths) { - if (substr($clazz, 0, $prefixLength) !== $prefix) { - // not prefixed with our add-on ID, ignore it - continue; - } - - $clazzWithoutPrefix = substr($clazz, $prefixLength); - $realClazz = $clazzWithoutPrefix; - $realPath = DevHelper_Generator_File::getClassPath($realClazz); - if (!file_exists($realPath)) { - // the real class could not be found, hmm... - // try to replace 'Extend_' with 'XenForo_' to support our legacy add-ons - if (substr($realClazz, 0, 7) !== 'Extend_') { - // no hope - continue; - } - - $realClazz = 'XenForo_' . substr($realClazz, 7); - $realPath = DevHelper_Generator_File::getClassPath($realClazz); - if (!file_exists($realPath)) { - // not found either... bye! - continue; - } - } - - if (self::generateXfcpClass($clazz, $realClazz, $config)) { - echo "Generated XFCP_{$clazz} ({$realClazz})\n"; - } - } - } - - public static function generateOurClass($clazz, DevHelper_Config_Base $config) - { - $path = DevHelper_Generator_File::getClassPath($clazz, $config); - $contents = <<getClassPrefix(), $config->getClassPrefix() . '_DevHelper_XFCP', $clazz); - $ghostPath = DevHelper_Generator_File::getClassPath($ghostClazz, $config); - if (file_exists($ghostPath)) { - // ghost file exists, yay! - return true; - } - - $ghostContents = "[^\s]+)\s#'; - while (true) { - if (preg_match($extendsXfcp, $contents, $matches, PREG_OFFSET_CAPTURE, $offset)) { - $clazz = $matches['clazz'][0]; - $offset = $matches[0][1] + strlen($matches[0][0]); - self::$_foundXfcpClasses[$clazz][$path] = true; - } else { - break; - } - } - } -} diff --git a/library/DevHelper/Installer.php b/library/DevHelper/Installer.php deleted file mode 100644 index 079832c..0000000 --- a/library/DevHelper/Installer.php +++ /dev/null @@ -1,107 +0,0 @@ -setupAutoloader($fileDir . '/library'); - XenForo_Application::initialize($fileDir . '/library', $fileDir); - - $dependencies = new XenForo_Dependencies_Public(); - $dependencies->preLoadData(); - - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = XenForo_Model::create('XenForo_Model_AddOn'); - - $devHelperAddOn = $addOnModel->getAddOnById('devHelper'); - if (!empty($devHelperAddOn)) { - die("DevHelper add-on has already been installed.\n"); - } - - $addOnModel->installAddOnXmlFromFile('library/DevHelper/addon-devHelper.xml'); - die("DevHelper add-on has been installed successfully.\n"); -} - -class DevHelper_Installer -{ - /* Start auto-generated lines of code. Change made will be overwriten... */ - - protected static $_tables = array(); - protected static $_patches = array(); - - public static function install($existingAddOn, $addOnData) - { - $db = XenForo_Application::get('db'); - - foreach (self::$_tables as $table) { - $db->query($table['createQuery']); - } - - foreach (self::$_patches as $patch) { - $tableExisted = $db->fetchOne($patch['showTablesQuery']); - if (empty($tableExisted)) { - continue; - } - - $existed = $db->fetchOne($patch['showColumnsQuery']); - if (empty($existed)) { - $db->query($patch['alterTableAddColumnQuery']); - } - } - - self::installCustomized($existingAddOn, $addOnData); - } - - public static function uninstall() - { - $db = XenForo_Application::get('db'); - - foreach (self::$_patches as $patch) { - $tableExisted = $db->fetchOne($patch['showTablesQuery']); - if (empty($tableExisted)) { - continue; - } - - $existed = $db->fetchOne($patch['showColumnsQuery']); - if (!empty($existed)) { - $db->query($patch['alterTableDropColumnQuery']); - } - } - - foreach (self::$_tables as $table) { - $db->query($table['dropQuery']); - } - - self::uninstallCustomized(); - } - - /* End auto-generated lines of code. Feel free to make changes below */ - - public static function installCustomized($existingAddOn, $addOnData) - { - // customized install script goes here - } - - public static function uninstallCustomized() - { - // customized uninstall script goes here - } - - public static function checkAddOnVersion() - { - if (XenForo_Application::isRegistered('addOns')) { - $addOns = XenForo_Application::get('addOns'); - $versionId = $addOns['devHelper']; - - $xml = file_get_contents(dirname(__FILE__) . '/addon-DevHelper.xml'); - if (preg_match('#version_id="(?\d+)"#', $xml, $matches)) { - if ($versionId < $matches['id']) { - return false; - } - } - } - - return true; - } -} diff --git a/library/DevHelper/Listener.php b/library/DevHelper/Listener.php deleted file mode 100755 index 770a5ca..0000000 --- a/library/DevHelper/Listener.php +++ /dev/null @@ -1,175 +0,0 @@ - $styleDate) { - // consider this is a change, start updating the template - $maxTemplateDate = max($maxTemplateDate, $templateDate); - - $templateId = DevHelper_Helper_Template::getTemplateIdFromFilePath($templateFile); - if (empty($templateId)) { - continue; - } - - $templateText = file_get_contents($templateFile); - - $properties = $propertyModel->keyPropertiesByName($propertyModel->getEffectiveStylePropertiesInStyle(0)); - $propertyChanges = $propertyModel->translateEditorPropertiesToArray( - $templateText, - $templateText, - $properties - ); - - /** @var XenForo_DataWriter_Template $dw */ - $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template'); - $dw->setExistingData($templateId); - $dw->set('template', $templateText); - - if ($dw->hasErrors()) { - throw new XenForo_Exception(implode(', ', $dw->getErrors())); - } - - if ($dw->hasChanges()) { - $dw->reparseTemplate(); - - $dw->save(); - - $propertyModel->saveStylePropertiesInStyleFromTemplate(0, $propertyChanges, $properties); - - $templatesUpdated[] = $dw->get('title'); - } - } - } - - if (!empty($maxTemplateDate)) { - /** @var XenForo_Model_Style $styleModel */ - $styleModel = XenForo_Model::create('XenForo_Model_Style'); - $styleModel->updateAllStylesLastModifiedDate($maxTemplateDate); - } - } - } - - public static function template_hook($hookName, &$contents, array $hookParams, XenForo_Template_Abstract $template) - { - switch ($hookName) { - case 'devhelper_devhelper_helper_addon_unit': - self::_filterDisabledAddOnOptions($contents); - break; - } - } - - protected static function _filterDisabledAddOnOptions(&$html) - { - $offset = 0; - - $addOns = XenForo_Application::get('addOns'); - - while (true) { - if (preg_match( - '/\s+<\/optgroup>/i', - $html, - $matches, - PREG_OFFSET_CAPTURE, - $groupOffset - )) { - $groupOffset = $matches[0][1] + 1; - $length = strlen($matches[0][0]); - - $html = substr_replace($html, '', $groupOffset - 1, $length); - $groupOffset--; - } else { - break; - } - } - } - - public static function file_health_check(XenForo_ControllerAdmin_Abstract $controller, array &$hashes) - { - $hashes += DevHelper_FileSums::getHashes(); - } -} diff --git a/library/DevHelper/Model/Config.php b/library/DevHelper/Model/Config.php deleted file mode 100755 index 1e1ddb3..0000000 --- a/library/DevHelper/Model/Config.php +++ /dev/null @@ -1,48 +0,0 @@ -upgradeConfig(); - - if (!empty($created) OR !empty($upgraded)) { - $this->saveAddOnConfig($addOn, $config); - } - - return $config; - } - - public function saveAddOnConfig($addOn, DevHelper_Config_Base $config) - { - $className = DevHelper_Generator_File::getClassName($addOn['addon_id'], 'DevHelper_Config'); - DevHelper_Generator_File::writeClass($className, $config->outputSelf()); - } -} diff --git a/library/DevHelper/Router.php b/library/DevHelper/Router.php deleted file mode 100644 index 7b9418e..0000000 --- a/library/DevHelper/Router.php +++ /dev/null @@ -1,154 +0,0 @@ -setupAutoloader($fileDir . '/xenforo/library'); - } - - public static function locateCached($fullPath) - { - $key = basename($fullPath) . md5($fullPath); - $cached = apcu_fetch($key); - $located = null; - $success = false; - if (is_array($cached) - && $cached['fullPath'] === $fullPath) { - $located = $cached['located']; - } - - if (empty($located)) { - $located = static::locate($fullPath, $success); - } - - if ($success) { - $cacheEntry = array( - 'fullPath' => $fullPath, - 'located' => $located, - ); - - apcu_store($key, $cacheEntry); - } - - return $located; - } - - public static function locate($fullPath, &$success = null) - { - if (file_exists($fullPath)) { - $success = true; - return $fullPath; - } - - list($xenforoDir, $addOnPaths) = static::getLocatePaths(); - $shortened = preg_replace( - '#^' . preg_quote($xenforoDir, '#') . '#', - '', - $fullPath - ); - - foreach ($addOnPaths as $addOnPath) { - $candidatePath = $addOnPath . $shortened; - if (file_exists($candidatePath)) { - $success = true; - return $candidatePath; - } - } - - return $fullPath; - } - - public static function getLocatePaths() - { - static $xenforoDir = null; - static $addOnPaths = null; - - if ($addOnPaths === null) { - $routerPhp = $_SERVER['DEVHELPER_ROUTER_PHP']; - $routerPhpDir = dirname($routerPhp); - $xenforoDir = sprintf('%s/xenforo', $routerPhpDir); - $addOnPaths = array($routerPhpDir); - - $txtPath = sprintf('%s/internal_data/addons.txt', $xenforoDir); - $lines = array(); - if (file_exists($txtPath)) { - $lines = file($txtPath); - } - - foreach ($lines as $line) { - $line = trim($line); - if (strlen($line) === 0) { - continue; - } - - $addOnPaths[] = sprintf('%s/%s', $routerPhpDir, $line); - } - } - - return array($xenforoDir, $addOnPaths); - } -} diff --git a/library/DevHelper/ViewAdmin/AddOn/DataManager.php b/library/DevHelper/ViewAdmin/AddOn/DataManager.php deleted file mode 100755 index db01efd..0000000 --- a/library/DevHelper/ViewAdmin/AddOn/DataManager.php +++ /dev/null @@ -1,18 +0,0 @@ -_params['dataClasses'] as &$dataClass) { - $dataClass['fieldsList'] = implode(', ', array_keys($dataClass['fields'])); - } - - if (!empty($this->_params['focusedDataClass'])) { - $this->_params['focusedDataClass']['sqlCreate'] = DevHelper_Generator_Db::createTable( - $this->_params['config'], - $this->_params['focusedDataClass'] - ); - } - } -} diff --git a/library/DevHelper/XenForo/CodeEvent.php b/library/DevHelper/XenForo/CodeEvent.php deleted file mode 100644 index e108e10..0000000 --- a/library/DevHelper/XenForo/CodeEvent.php +++ /dev/null @@ -1,113 +0,0 @@ -getRootDir() . '/library/XenForo/CodeEvent.php'); -$contents = substr($originalContents, strpos($originalContents, 'autoload('DevHelper_Installer'); - } - - $addOn = $addOnModel->getAddOnById('devHelper'); - if (!empty($addOn)) { - /** @var XenForo_DataWriter_AddOn $addOnDw */ - $addOnDw = XenForo_DataWriter::create('XenForo_DataWriter_AddOn'); - $addOnDw->setExistingData($addOn); - $addOnDw->set('active', 1); - $addOnDw->save(); - - $message = 'Re-enabled DevHelper add-on.'; - } else { - $xmlPath = 'library/DevHelper/addon-devHelper.xml'; - if (!file_exists($xmlPath)) { - die(sprintf('XML path (%s) does not exist', $xmlPath)); - } - $addOnModel->installAddOnXmlFromFile($xmlPath); - - $message = 'Installed DevHelper add-on.'; - } - - /** @var XenForo_FrontController $fc */ - $fc = $args[0]; - $redirect = $fc->getRequest()->getRequestUri(); - - die(sprintf( - 'alert(%s);window.location = %s;', - json_encode($message), - json_encode(XenForo_Link::buildAdminLink('full:tools/run-deferred', null, array( - 'redirect' => $redirect, - ))) - )); - } - } - - return parent::fire($event, $args, $hint); - } - - public static $measuredTime = array('events' => array(), 'callbacks' => array()); - - public static function DevHelper_measureCallbackTime($event, $callback, $args) - { - $startTime = microtime(true); - $result = call_user_func_array($callback, $args); - $elapsed = microtime(true) - $startTime; - - if (!isset(self::$measuredTime['events'][$event])) { - self::$measuredTime['events'][$event] = array( - 'count' => 0, - 'elapsed' => 0, - ); - } - self::$measuredTime['events'][$event]['count']++; - self::$measuredTime['events'][$event]['elapsed'] += $elapsed; - - $callbackSafe = $callback; - if (!is_string($callbackSafe)) { - $callbackSafe = XenForo_Helper_Php::safeSerialize($callback); - } - if (!isset(self::$measuredTime['callbacks'][$callbackSafe])) { - self::$measuredTime['callbacks'][$callbackSafe] = array( - 'count' => 0, - 'elapsed' => 0, - ); - } - self::$measuredTime['callbacks'][$callbackSafe]['count']++; - self::$measuredTime['callbacks'][$callbackSafe]['elapsed'] += $elapsed; - - return $result; - } -} - -eval('abstract class XenForo_CodeEvent extends DevHelper_XenForo_CodeEvent {}'); - -if (false) { - class _XenForo_CodeEvent extends XenForo_CodeEvent - { - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/AddOn.php b/library/DevHelper/XenForo/ControllerAdmin/AddOn.php deleted file mode 100755 index b6fa361..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/AddOn.php +++ /dev/null @@ -1,387 +0,0 @@ -getHelper('DevHelper_ControllerHelper_AddOn'); - $helper->selfCheck(); - - $response = parent::actionIndex(); - - if ($response instanceof XenForo_ControllerResponse_View) { - uasort($response->params['addOns'], create_function( - '$a, $b', - 'if ($a["active"] == $b["active"]) { return strcmp($a["title"], $b["title"]); } else { if ($a["active"]) return -1; else return 1; }' - )); - } - - return $response; - } - - public function actionInstall() - { - if (!$this->isConfirmedPost() - && !empty($_SERVER['DEVHELPER_ROUTER_PHP']) - ) { - exec('/usr/local/bin/find-addons.sh'); - apcu_clear_cache(); - } - - return parent::actionInstall(); - } - - public function actionSave() - { - $GLOBALS[DevHelper_Listener::XENFORO_CONTROLLERADMIN_ADDON_SAVE] = $this; - - $response = parent::actionSave(); - - if ($response instanceof XenForo_ControllerResponse_Redirect) { - if (!empty($this->_DevHelper_actionSave_addOnIdChanged)) { - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('add-ons/file-export', $this->_DevHelper_actionSave_addOnIdChanged) - ); - } - } - - return $response; - } - - public function DevHelper_actionSave(XenForo_DataWriter_AddOn $addOnDw) - { - if ($addOnDw->isUpdate() AND $addOnDw->isChanged('version_id')) { - $this->_DevHelper_actionSave_addOnIdChanged = $addOnDw->getMergedData(); - } - } - - public function actionDataManager() - { - $addOnId = $this->_input->filterSingle('addon_id', XenForo_Input::STRING); - $addOn = $this->_getAddOnOrError($addOnId); - - $config = $this->_getConfigModel()->loadAddOnConfig($addOn); - - $viewParams = array( - 'addOn' => $addOn, - 'config' => $config, - 'dataClasses' => $config->getDataClasses(), - ); - - $dataClassName = $this->_input->filterSingle('dataClass', XenForo_Input::STRING); - if (!empty($dataClassName) AND $config->checkDataClassExists($dataClassName)) { - $viewParams['focusedDataClass'] = $config->getDataClass($dataClassName); - } - - return $this->responseView( - 'DevHelper_ViewAdmin_AddOn_DataManager', - 'devhelper_addon_data_manager', - $viewParams - ); - } - - public function actionGenerateFile() - { - $addOnId = $this->_input->filterSingle('addon_id', XenForo_Input::STRING); - $addOn = $this->_getAddOnOrError($addOnId); - - $config = $this->_getConfigModel()->loadAddOnConfig($addOn); - $dataClassName = $this->_input->filterSingle('dataClass', XenForo_Input::STRING); - if (!empty($dataClassName) AND $config->checkDataClassExists($dataClassName)) { - $dataClass = $config->getDataClass($dataClassName); - $file = $this->_input->filterSingle('file', XenForo_Input::STRING); - - switch ($file) { - case 'data_writer': - case 'model': - case 'route_prefix_admin': - case 'controller_admin': - case 'installer': - $suffix = str_replace(' ', '', ucwords(str_replace('_', ' ', $file))); - return call_user_func(array( - $this, - '_actionGenerate' . $suffix - ), $addOn, $config, $dataClass); - } - } - - return $this->responseNoPermission(); - } - - protected function _actionGenerateDataWriter(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - list($className, $contents) = DevHelper_Generator_Code_DataWriter::generate($addOn, $config, $dataClass); - $path = DevHelper_Generator_File::writeClass($className, $contents); - - $config->updateDataClassFile($dataClass['name'], 'data_writer', $className, $path); - $this->_getConfigModel()->saveAddOnConfig($addOn, $config); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::RESOURCE_CREATED, - XenForo_Link::buildAdminLink( - 'add-ons/data-manager', - $addOn, - array('dataClass' => $dataClass['name']) - ) - ); - } - - protected function _actionGenerateModel(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - list($className, $contents) = DevHelper_Generator_Code_Model::generate($addOn, $config, $dataClass); - $path = DevHelper_Generator_File::writeClass($className, $contents); - - $config->updateDataClassFile($dataClass['name'], 'model', $className, $path); - $this->_getConfigModel()->saveAddOnConfig($addOn, $config); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::RESOURCE_CREATED, - XenForo_Link::buildAdminLink( - 'add-ons/data-manager', - $addOn, - array('dataClass' => $dataClass['name']) - ) - ); - } - - protected function _actionGenerateRoutePrefixAdmin(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $routePrefix = $this->_input->filterSingle('route_prefix', XenForo_Input::STRING); - $controller = $this->_input->filterSingle('controller', XenForo_Input::STRING); - $majorSection = $this->_input->filterSingle('major_section', XenForo_Input::STRING); - - if (empty($routePrefix) OR empty($controller)) { - $viewParams = array( - 'addOn' => $addOn, - 'dataClass' => $dataClass, - 'routePrefix' => $routePrefix ?: DevHelper_Generator_Code_RoutePrefixAdmin::getRoutePrefix( - $addOn, - $config, - $dataClass - ), - 'controller' => $controller ?: DevHelper_Generator_Code_ControllerAdmin::getClassName( - $addOn, - $config, - $dataClass - ), - 'majorSection' => $majorSection, - ); - - return $this->responseView( - 'DevHelper_ViewAdmin_AddOn_GenerateRoutePrefixAdmin', - 'devhelper_addon_generate_route_prefix_admin', - $viewParams - ); - } else { - list($className, $contents) = DevHelper_Generator_Code_RoutePrefixAdmin::generate( - $addOn, - $config, - $dataClass, - array( - 'routePrefix' => $routePrefix, - 'controller' => $controller, - 'majorSection' => $majorSection, - ) - ); - $path = DevHelper_Generator_File::writeClass($className, $contents); - - $config->updateDataClassFile($dataClass['name'], 'route_prefix_admin', $className, $path); - $this->_getConfigModel()->saveAddOnConfig($addOn, $config); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::RESOURCE_CREATED, - XenForo_Link::buildAdminLink( - 'add-ons/data-manager', - $addOn, - array('dataClass' => $dataClass['name']) - ) - ); - } - } - - protected function _actionGenerateControllerAdmin(array $addOn, DevHelper_Config_Base $config, array $dataClass) - { - $routePrefix = $this->_input->filterSingle('route_prefix', XenForo_Input::STRING); - $controller = $this->_input->filterSingle('controller', XenForo_Input::STRING); - - if (empty($routePrefix) OR empty($controller)) { - $viewParams = array( - 'addOn' => $addOn, - 'dataClass' => $dataClass, - 'routePrefix' => $routePrefix ?: DevHelper_Generator_Code_RoutePrefixAdmin::getRoutePrefix( - $addOn, - $config, - $dataClass - ), - 'controller' => $controller ?: DevHelper_Generator_Code_ControllerAdmin::getClassName( - $addOn, - $config, - $dataClass - ), - ); - - return $this->responseView( - 'DevHelper_ViewAdmin_AddOn_GenerateControllerAdmin', - 'devhelper_addon_generate_controller_admin', - $viewParams - ); - } else { - list($className, $contents) = DevHelper_Generator_Code_ControllerAdmin::generate( - $addOn, - $config, - $dataClass, - array( - 'routePrefix' => $routePrefix, - 'controller' => $controller, - ) - ); - $path = DevHelper_Generator_File::writeClass($className, $contents); - - $config->updateDataClassFile($dataClass['name'], 'controller_admin', $className, $path); - $this->_getConfigModel()->saveAddOnConfig($addOn, $config); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::RESOURCE_CREATED, - XenForo_Link::buildAdminLink( - 'add-ons/data-manager', - $addOn, - array('dataClass' => $dataClass['name']) - ) - ); - } - } - - public function actionGenerateInstaller() - { - $addOnId = $this->_input->filterSingle('addon_id', XenForo_Input::STRING); - $addOn = $this->_getAddOnOrError($addOnId); - - $config = $this->_getConfigModel()->loadAddOnConfig($addOn); - - list($className, $contents) = DevHelper_Generator_Code_Installer::generate($addOn, $config); - DevHelper_Generator_File::writeClass($className, $contents, $config); - - $dw = XenForo_DataWriter::create('XenForo_DataWriter_AddOn'); - $dw->setExistingData($addOn); - $dw->set('install_callback_class', $className); - $dw->set('install_callback_method', 'install'); - $dw->set('uninstall_callback_class', $className); - $dw->set('uninstall_callback_method', 'uninstall'); - $dw->save(); - - $xmlPath = DevHelper_Generator_File::getAddOnXmlPath($addOn, null, $config); - $this->_getAddOnModel()->getAddOnXml($addOn)->save($xmlPath); - - if ($this->_input->filterSingle('run', XenForo_Input::UINT)) { - call_user_func(array( - $className, - 'install' - ), $dw->getMergedData(), $dw->getMergedData()); - } - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::RESOURCE_CREATED, - XenForo_Link::buildAdminLink('add-ons/data-manager', $addOn) - ); - } - - public function actionFileExport() - { - $addOnId = $this->_input->filterSingle('addon_id', XenForo_Input::STRING); - $addOn = $this->_getAddOnOrError($addOnId); - - $config = $this->_getConfigModel()->loadAddOnConfig($addOn); - - $exportPath = $config->getExportPath(); - - if ($exportPath === false) { - return $this->responseNoPermission(); - } - - DevHelper_Helper_Phrase::startLookingForPhraseTitles(); - DevHelper_Helper_Xfcp::startLookingForXfcpClasses(); - - echo '
';
-
-        DevHelper_Generator_File::fileExport($addOn, $config, $exportPath);
-
-        /** @var XenForo_Model_Template $templateModel */
-        $templateModel = $this->getModelFromCache('XenForo_Model_Template');
-        $templates = $templateModel->getMasterTemplatesInAddOn($addOn['addon_id']);
-        foreach ($templates AS $template) {
-            /** @var DevHelper_XenForo_DataWriter_Template $dw */
-            $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
-            $dw->setExistingData($template, true);
-
-            if ($dw->DevHelper_saveTemplate()) {
-                echo "Saved template {$template['title']}\n";
-            }
-        }
-
-        /** @var XenForo_Model_Phrase $phraseModel */
-        $phraseModel = $this->getModelFromCache('XenForo_Model_Phrase');
-        $phrases = $phraseModel->getMasterPhrasesInAddOn($addOnId);
-        DevHelper_Helper_Phrase::finishLookingForPhraseTitles($phrases, $phraseModel);
-        DevHelper_Helper_Xfcp::finishLookingForXfcpClasses($addOn, $config);
-
-        echo '
'; - - die('Done'); - } - - public function actionSwitchContext() - { - $addOnId = $this->_input->filterSingle('addon_id', XenForo_Input::STRING); - $targetAddOn = $this->_getAddOnOrError($addOnId); - - $targetAddOnDw = XenForo_DataWriter::create('XenForo_DataWriter_AddOn'); - $targetAddOnDw->setExistingData($targetAddOn, true); - $targetAddOnDw->set('active', 1); - $targetAddOnDw->save(); - - DevHelper_Generator_Code_XenForoConfig::updateConfig('development.default_addon', $targetAddOn['addon_id']); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('add-ons') - ); - } - - public function actionAllOff() - { - $addOns = $this->_getAddOnModel()->getAllAddOns(); - - foreach ($addOns as $addOn) { - if (empty($addOn['active'])) { - continue; - } - - if ($addOn['addon_id'] == 'devHelper') { - continue; - } - - $addOnDw = XenForo_DataWriter::create('XenForo_DataWriter_AddOn'); - $addOnDw->setExistingData($addOn, true); - $addOnDw->set('active', 0); - $addOnDw->save(); - } - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('add-ons') - ); - } - - /** - * @return DevHelper_Model_Config - */ - protected function _getConfigModel() - { - /** @noinspection PhpIncompatibleReturnTypeInspection */ - return $this->getModelFromCache('DevHelper_Model_Config'); - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/AdminTemplateModification.php b/library/DevHelper/XenForo/ControllerAdmin/AdminTemplateModification.php deleted file mode 100644 index 1a75f99..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/AdminTemplateModification.php +++ /dev/null @@ -1,21 +0,0 @@ -params['addOns']; - $groupedModifications = &$response->params['groupedModifications']; - $modificationCount = &$response->params['modificationCount']; - - /** @var DevHelper_ControllerHelper_AddOn $helper */ - $helper = $this->getHelper('DevHelper_ControllerHelper_AddOn'); - $modificationCount -= $helper->filterKeepActiveAddOns($groupedModifications, $addOns); - } - - return $response; - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/CodeEventListener.php b/library/DevHelper/XenForo/ControllerAdmin/CodeEventListener.php deleted file mode 100755 index 80d492a..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/CodeEventListener.php +++ /dev/null @@ -1,69 +0,0 @@ -params['addOns']; - $listeners = &$response->params['listeners']; - $totalListeners =& $response->params['totalListeners']; - - /** @var DevHelper_ControllerHelper_AddOn $helper */ - $helper = $this->getHelper('DevHelper_ControllerHelper_AddOn'); - $totalListeners -= $helper->filterKeepActiveAddOns($listeners, $addOns); - } - - return $response; - } - - public function actionSave() - { - $dwInput = $this->_input->filter(array( - 'event_id' => XenForo_Input::STRING, - 'description' => XenForo_Input::STRING, - 'callback_class' => XenForo_Input::STRING, - 'callback_method' => XenForo_Input::STRING, - 'hint' => XenForo_Input::STRING, - 'addon_id' => XenForo_Input::STRING, - )); - - if (!empty($dwInput['event_id']) - && empty($dwInput['description']) - && empty($dwInput['callback_class']) - && empty($dwInput['callback_class']) - && !empty($dwInput['hint']) - && !empty($dwInput['addon_id']) - ) { - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = $this->getModelFromCache('XenForo_Model_AddOn'); - $addOn = $addOnModel->getAddOnById($dwInput['addon_id']); - - /** @var DevHelper_Model_Config $configModel */ - $configModel = $this->getModelFromCache('DevHelper_Model_Config'); - $config = $configModel->loadAddOnConfig($addOn); - - if (strpos($dwInput['event_id'], 'load_class') === 0) { - $classPath = DevHelper_Generator_File::getClassPath($dwInput['hint']); - if (is_file($classPath)) { - $method = DevHelper_Generator_Code_Listener::generateLoadClass($dwInput['hint'], $addOn, $config); - if ($method) { - $clazz = DevHelper_Generator_Code_Listener::getClassName($addOn, $config); - - $this->_request->setParam('description', $dwInput['hint']); - $this->_request->setParam('callback_class', $clazz); - $this->_request->setParam('callback_method', $method); - - XenForo_DataWriter::create('XenForo_DataWriter_CodeEventListener'); - DevHelper_XenForo_DataWriter_CodeEventListener::DevHelper_markAsGeneratedCallback($clazz, $method); - } - } - } - } - - return parent::actionSave(); - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/Home.php b/library/DevHelper/XenForo/ControllerAdmin/Home.php deleted file mode 100644 index 8200103..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/Home.php +++ /dev/null @@ -1,13 +0,0 @@ -getHelper('DevHelper_ControllerHelper_AddOn'); - $helper->selfCheck(); - - return parent::actionIndex(); - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/Permission.php b/library/DevHelper/XenForo/ControllerAdmin/Permission.php deleted file mode 100644 index 11cf1cb..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/Permission.php +++ /dev/null @@ -1,41 +0,0 @@ -params['permissionGroups']; - $permissionsGrouped = &$response->params['permissionsGrouped']; - $permissionsUngrouped = &$response->params['permissionsUngrouped']; - $interfaceGroups = &$response->params['interfaceGroups']; - $totalPermissions = &$response->params['totalPermissions']; - - $removedCount = 0; - - /** @var DevHelper_ControllerHelper_AddOn $helper */ - $helper = $this->getHelper('DevHelper_ControllerHelper_AddOn'); - $helper->filterKeepActiveAddOnsDirect($permissionGroups); - $helper->filterKeepActiveAddOnsDirect($interfaceGroups); - - foreach (array_keys($permissionsGrouped) as $interfaceGroupId) { - if (empty($interfaceGroups[$interfaceGroupId])) { - $removedCount += count($permissionsGrouped[$interfaceGroupId]); - unset($permissionsGrouped[$interfaceGroupId]); - } - } - - foreach ($permissionsGrouped as &$groupPermissions) { - $removedCount += $helper->filterKeepActiveAddOnsDirect($groupPermissions); - } - - $removedCount += $helper->filterKeepActiveAddOnsDirect($permissionsUngrouped); - - $totalPermissions -= $removedCount; - } - - return $response; - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/Phrase.php b/library/DevHelper/XenForo/ControllerAdmin/Phrase.php deleted file mode 100644 index 596fd01..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/Phrase.php +++ /dev/null @@ -1,28 +0,0 @@ -isSuperAdmin() - && $this->_input->filterSingle('language_id', XenForo_Input::UINT) > 0 - ) { - return $this->responseError('Please use non-super admin account to create non-master phrase.'); - } - - return parent::actionAdd(); - } - - public function actionEdit() - { - if (XenForo_Application::debugMode() - && XenForo_Visitor::getInstance()->isSuperAdmin() - && $this->_input->filterSingle('language_id', XenForo_Input::UINT) > 0 - ) { - return $this->responseError('Please use non-super admin account to edit non-master phrase.'); - } - - return parent::actionEdit(); - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/RoutePrefix.php b/library/DevHelper/XenForo/ControllerAdmin/RoutePrefix.php deleted file mode 100644 index 6e743e9..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/RoutePrefix.php +++ /dev/null @@ -1,26 +0,0 @@ -params['publicPrefixes']; - $adminPrefixes = &$response->params['adminPrefixes']; - $totalPrefixes = &$response->params['totalPrefixes']; - - $removedCount = 0; - - /** @var DevHelper_ControllerHelper_AddOn $helper */ - $helper = $this->getHelper('DevHelper_ControllerHelper_AddOn'); - $removedCount += $helper->filterKeepActiveAddOnsDirect($publicPrefixes); - $removedCount += $helper->filterKeepActiveAddOnsDirect($adminPrefixes); - - $totalPrefixes -= $removedCount; - } - - return $response; - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/Template.php b/library/DevHelper/XenForo/ControllerAdmin/Template.php deleted file mode 100644 index 15725d8..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/Template.php +++ /dev/null @@ -1,28 +0,0 @@ -isSuperAdmin() - && $this->_input->filterSingle('style_id', XenForo_Input::UINT) > 0 - ) { - return $this->responseError('Please use non-super admin account to create non-master template.'); - } - - return parent::actionAdd(); - } - - public function actionEdit() - { - if (XenForo_Application::debugMode() - && XenForo_Visitor::getInstance()->isSuperAdmin() - && $this->_input->filterSingle('style_id', XenForo_Input::UINT) > 0 - ) { - return $this->responseError('Please use non-super admin account to edit non-master template.'); - } - - return parent::actionEdit(); - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/TemplateModification.php b/library/DevHelper/XenForo/ControllerAdmin/TemplateModification.php deleted file mode 100644 index 0c2d53a..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/TemplateModification.php +++ /dev/null @@ -1,21 +0,0 @@ -params['addOns']; - $groupedModifications = &$response->params['groupedModifications']; - $modificationCount = &$response->params['modificationCount']; - - /** @var DevHelper_ControllerHelper_AddOn $helper */ - $helper = $this->getHelper('DevHelper_ControllerHelper_AddOn'); - $modificationCount -= $helper->filterKeepActiveAddOns($groupedModifications, $addOns); - } - - return $response; - } -} diff --git a/library/DevHelper/XenForo/ControllerAdmin/Tools.php b/library/DevHelper/XenForo/ControllerAdmin/Tools.php deleted file mode 100644 index aace131..0000000 --- a/library/DevHelper/XenForo/ControllerAdmin/Tools.php +++ /dev/null @@ -1,190 +0,0 @@ -responseNoPermission(); - } - - /** @var XenForo_Model_AddOn $addOnModel */ - $addOnModel = $this->getModelFromCache('XenForo_Model_AddOn'); - - $addOn = $addOnModel->getAddOnById('devHelper'); - $xmlPath = DevHelper_Generator_File::getAddOnXmlPath($addOn); - - $addOnModel->installAddOnXmlFromFile($xmlPath, $addOn['addon_id']); - - return $this->responseRedirect( - XenForo_ControllerResponse_Redirect::SUCCESS, - XenForo_Link::buildAdminLink('index') - ); - } - - public function actionAddOnsServerFile() - { - $q = $this->_input->filterSingle('q', XenForo_Input::STRING); - $matchedPaths = array(); - - /** @var XenForo_Application $app */ - $app = XenForo_Application::getInstance(); - $rootDir = $app->getRootDir(); - $scanPath = ''; - - if (strlen($q) > 0 - && strpos($q, '.') === false - ) { - if (strlen($q) < 7) { - $originalQuery = $q; - $q = 'library/'; - $matchedPaths[] = 'library'; - - if (isset($_SERVER['DEVHELPER_ROUTER_PHP'])) { - $routerPhp = $_SERVER['DEVHELPER_ROUTER_PHP']; - $routerPhpDir = dirname($routerPhp); - $matchedPaths[] = sprintf('%s/addons', $routerPhpDir); - - list(, $addOnPaths) = DevHelper_Router::getLocatePaths(); - foreach ($addOnPaths as $addOnPath) { - if (stripos($addOnPath, $originalQuery) !== false) { - array_unshift($matchedPaths, $addOnPath); - } - } - } - } - - $parts = preg_split('#/#', $q, -1, PREG_SPLIT_NO_EMPTY); - - $prefix = ''; - if (count($parts) > 0) { - $prefix = array_pop($parts); - } - - if (substr($q, 0, 1) === '/') { - $scanPath = '/' . implode('/', $parts); - } else { - $scanPath = rtrim(sprintf('%s/%s', $rootDir, implode('/', $parts)), '/'); - } - - $pathWithPrefix = $scanPath . '/' . $prefix; - if (is_dir($pathWithPrefix)) { - $scanPath = $pathWithPrefix; - $prefix = ''; - } - - if (is_dir($scanPath)) { - $contentPaths = glob(sprintf('%s/*', $scanPath)); - - foreach ($contentPaths as $contentPath) { - if ($prefix !== '' - && substr(basename($contentPath), 0, strlen($prefix)) !== $prefix - ) { - continue; - } - - if (is_dir($contentPath)) { - $matchedPaths[] = $contentPath . '/'; - } else { - $ext = XenForo_Helper_File::getFileExtension($contentPath); - if ($ext === 'xml') { - array_unshift($matchedPaths, $contentPath); - } - } - } - } - - $xmlPath = DevHelper_Helper_File::findXml($matchedPaths); - if (!empty($xmlPath)) { - array_unshift($matchedPaths, $xmlPath); - } - } - - $results = array(); - foreach ($matchedPaths as $matchedPath) { - $relativePath = preg_replace('#^' . preg_quote($app->getRootDir()) . '/#', '', $matchedPath); - $results[$relativePath] = basename($matchedPath); - - if (substr($matchedPath, 0, strlen($scanPath)) === $scanPath) { - $results[$relativePath] = ltrim(substr_replace($matchedPath, '', 0, strlen($scanPath)), '/'); - } - } - - $view = $this->responseView(); - $view->jsonParams = array( - 'results' => $results - ); - return $view; - } - - public function actionCodeEventListenersHint() - { - $q = $this->_input->filterSingle('q', XenForo_Input::STRING); - $classes = array(); - - /** @var XenForo_Application $app */ - $app = XenForo_Application::getInstance(); - $libraryPath = sprintf('%s/library/', $app->getRootDir()); - - if (strlen($q) > 0 - && preg_match('/[A-Z]/', $q) - ) { - $dirPath = ''; - $pattern = ''; - - $classPath = DevHelper_Generator_File::getClassPath($q); - if (is_file($classPath)) { - $classes[] = $q; - } - - $_dirPath = preg_replace('/\.php$/', '', $classPath); - if (is_dir($_dirPath)) { - $dirPath = $_dirPath; - } else { - $_parentDirPath = dirname($_dirPath); - if (is_dir($_parentDirPath)) { - $dirPath = $_parentDirPath; - $pattern = basename($_dirPath); - } - } - - if ($dirPath !== '') { - $files = scandir($dirPath); - - foreach ($files as $file) { - if (substr($file, 0, 1) === '.') { - continue; - } - - if ($pattern !== '' - && strpos($file, $pattern) !== 0 - ) { - continue; - } - - $filePath = sprintf('%s/%s', $dirPath, $file); - - if (is_file($filePath)) { - $contents = file_get_contents($filePath); - if (preg_match('/class\s(?.+?)(\sextends|{)/', $contents, $matches)) { - $classes[] = $matches['class']; - } - } elseif (is_dir($filePath)) { - $classes[] = str_replace('/', '_', str_replace($libraryPath, '', $filePath)); - } - } - } - } - - $results = array(); - foreach ($classes as $class) { - $results[$class] = $class; - } - - $view = $this->responseView(); - $view->jsonParams = array( - 'results' => $results - ); - return $view; - } -} diff --git a/library/DevHelper/XenForo/ControllerHelper/Editor.php b/library/DevHelper/XenForo/ControllerHelper/Editor.php deleted file mode 100644 index 72714a8..0000000 --- a/library/DevHelper/XenForo/ControllerHelper/Editor.php +++ /dev/null @@ -1,26 +0,0 @@ -_DevHelper_disableAssertPostOnly = true; - - $limit = $this->_input->filterSingle('limit', XenForo_Input::UINT, array('default' => 10)); - for ($i = 0; $i < $limit; $i++) { - $this->_request->setParam('title', sprintf( - 'Thread Title %d-%d', - XenForo_Application::$time, - $i - )); - $this->_request->setParam('message', sprintf( - 'Thread Body %d-%d', - XenForo_Application::$time, - $i - )); - - try { - $response = $this->actionAddThread(); - } catch (Exception $e) { - return $this->responseError($e->getMessage()); - } - } - - return $response; - } - - protected function _assertPostOnly() - { - if ($this->_DevHelper_disableAssertPostOnly) { - return; - } - - parent::_assertPostOnly(); - } -} diff --git a/library/DevHelper/XenForo/ControllerPublic/Thread.php b/library/DevHelper/XenForo/ControllerPublic/Thread.php deleted file mode 100644 index 71c41db..0000000 --- a/library/DevHelper/XenForo/ControllerPublic/Thread.php +++ /dev/null @@ -1,38 +0,0 @@ -_DevHelper_disableAssertPostOnly = true; - - $limit = $this->_input->filterSingle('limit', XenForo_Input::UINT, array('default' => 10)); - for ($i = 0; $i < $limit; $i++) { - $this->_request->setParam('message', sprintf( - 'Post Body %d-%d', - XenForo_Application::$time, - $i - )); - - try { - $response = $this->actionAddReply(); - } catch (Exception $e) { - return $this->responseError($e->getMessage()); - } - } - - return $response; - } - - protected function _assertPostOnly() - { - if ($this->_DevHelper_disableAssertPostOnly) { - return; - } - - parent::_assertPostOnly(); - } -} diff --git a/library/DevHelper/XenForo/DataWriter/AddOn.php b/library/DevHelper/XenForo/DataWriter/AddOn.php deleted file mode 100644 index 397c955..0000000 --- a/library/DevHelper/XenForo/DataWriter/AddOn.php +++ /dev/null @@ -1,15 +0,0 @@ -DevHelper_actionSave($this); - } - - parent::_preSave(); - } -} diff --git a/library/DevHelper/XenForo/DataWriter/CodeEventListener.php b/library/DevHelper/XenForo/DataWriter/CodeEventListener.php deleted file mode 100644 index ef74056..0000000 --- a/library/DevHelper/XenForo/DataWriter/CodeEventListener.php +++ /dev/null @@ -1,29 +0,0 @@ -get('callback_class') - && $callback[1] === $this->get('callback_method') - ) { - // triggering error for our generated method - // this may happen if we modified an existing listener which had already been loaded to memory - // ignore it... - return; - } - } - } - - parent::error($error, $errorKey, $specificError); - } - - public static function DevHelper_markAsGeneratedCallback($clazz, $method) - { - self::$_DevHelper_generatedCallbacks[] = array($clazz, $method); - } -} diff --git a/library/DevHelper/XenForo/DataWriter/Template.php b/library/DevHelper/XenForo/DataWriter/Template.php deleted file mode 100644 index 7690b2c..0000000 --- a/library/DevHelper/XenForo/DataWriter/Template.php +++ /dev/null @@ -1,24 +0,0 @@ -getMergedData(); - $filePath = DevHelper_Helper_Template::getTemplateFilePath($template); - XenForo_Helper_File::createDirectory(dirname($filePath)); - - return file_put_contents($filePath, $template['template']) > 0; - } - - protected function _postSaveAfterTransaction() - { - $this->DevHelper_saveTemplate(); - - parent::_postSaveAfterTransaction(); - } -} diff --git a/library/DevHelper/XenForo/Debug.php b/library/DevHelper/XenForo/Debug.php deleted file mode 100644 index f7c0b81..0000000 --- a/library/DevHelper/XenForo/Debug.php +++ /dev/null @@ -1,52 +0,0 @@ -getRootDir() . '/library/XenForo/Debug.php'); -$contents = substr($originalContents, strpos($originalContents, ' $oneData) { - $rows[] = sprintf( - '%s%d%.6f', - $label, - $oneData['count'], - $oneData['elapsed'] - ); - } - - return sprintf('%s
', implode('', $rows)); - } -} - -eval('abstract class XenForo_Debug extends DevHelper_XenForo_Debug {}'); - -if (false) { - class _XenForo_Debug extends XenForo_Debug - { - } -} diff --git a/library/DevHelper/XenForo/Model/AddOn.php b/library/DevHelper/XenForo/Model/AddOn.php deleted file mode 100755 index e41113e..0000000 --- a/library/DevHelper/XenForo/Model/AddOn.php +++ /dev/null @@ -1,34 +0,0 @@ - $groupAddOnNames) { - $options[$groupId] = array(); - - foreach ($groupAddOnNames as $key => $addOnName) { - $options[$groupId][$key] = $addOnName; - } - } - } - - return $options; - } -} diff --git a/library/DevHelper/XenForo/Route/PrefixAdmin/Templates.php b/library/DevHelper/XenForo/Route/PrefixAdmin/Templates.php deleted file mode 100644 index af88dac..0000000 --- a/library/DevHelper/XenForo/Route/PrefixAdmin/Templates.php +++ /dev/null @@ -1,22 +0,0 @@ -getAction(); - if (strpos($action, '_') !== false && intval($request->getParam('template_id')) === 0) { - /** @var XenForo_Model_Template $templateModel */ - $templateModel = XenForo_Model::create('XenForo_Model_Template'); - $templateIds = $templateModel->getTemplateIdInStylesByTitle($action); - if (!empty($templateIds[0])) { - $request->setParam('template_id', $templateIds[0]); - $routeMatch->setAction('edit'); - } - } - - return $routeMatch; - } -} diff --git a/library/DevHelper/XenForo/Template/Abstract.php b/library/DevHelper/XenForo/Template/Abstract.php deleted file mode 100644 index 41bbb80..0000000 --- a/library/DevHelper/XenForo/Template/Abstract.php +++ /dev/null @@ -1,31 +0,0 @@ -getRootDir() . '/library/XenForo/Template/Abstract.php'); -$contents = substr($originalContents, strpos($originalContents, '_params['addOn']) && empty($this->_params['serverFile'])) { - $addOn = $this->_params['addOn']; - $this->_params['serverFile'] = DevHelper_Generator_File::getAddOnXmlPath($addOn); - } - - parent::prepareParams(); - } -} - -if (false) { - class XFCP_DevHelper_XenForo_ViewAdmin_AddOn_Upgrade extends XenForo_ViewAdmin_Base - { - } -} diff --git a/library/DevHelper/XenForo/ViewRenderer/Json.php b/library/DevHelper/XenForo/ViewRenderer/Json.php deleted file mode 100644 index d174c54..0000000 --- a/library/DevHelper/XenForo/ViewRenderer/Json.php +++ /dev/null @@ -1,71 +0,0 @@ -getRootDir() . '/library/XenForo/ViewRenderer/Json.php'); -$contents = substr($originalContents, strpos($originalContents, 'getProfiler(); - $params['_queryCount'] = $profiler->getTotalNumQueries(); - $params['_totalQueryRunTime'] = 0; - - if ($params['_queryCount']) { - $params['_queries'] = array(); - - $queries = $profiler->getQueryProfiles(); - - /** @var Zend_Db_Profiler_Query $query */ - foreach ($queries AS $query) { - $queryText = $query->getQuery(); - $queryText = preg_replace('#\s+#', ' ', $queryText); - $queryText = trim($queryText); - - foreach ($query->getQueryParams() AS $param) { - $param = sprintf('{%s}', htmlentities($param)); - $pos = strpos($queryText, '?'); - if ($pos !== false) { - $queryText = substr_replace($queryText, $param, $pos, 1); - } - } - - $params['_queries'][] = htmlentities($queryText); - $params['_totalQueryRunTime'] += $query->getElapsedSecs(); - } - } - } - - return $params; - } -} - -eval('class XenForo_ViewRenderer_Json extends DevHelper_XenForo_ViewRenderer_Json {}'); - -if (false) { - class _XenForo_ViewRenderer_Json extends XenForo_ViewRenderer_Json - { - } -} diff --git a/library/DevHelper/addon-devHelper.xml b/library/DevHelper/addon-devHelper.xml deleted file mode 100644 index da21956..0000000 --- a/library/DevHelper/addon-devHelper.xml +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - ]]> - {xen:phrase devhelper_data_manager} -{xen:phrase devhelper_file_export} - -$0]]> - - - - - - - ]]> - {xen:phrase devhelper_data_manager} -{xen:phrase devhelper_file_export} -{xen:phrase devhelper_switch_context} - -$0]]> - - - - - - - ]]> - ]]> - - - ]]> - ]]> - - - - - - - #]]> - $0]]> - - - ]]> - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..550261c --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,26 @@ + + + */.data/ + */_data/ + */_no_upload/ + */_output/ + */_releases/ + */node_modules/ + */vendor/ + *.min.js + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/prepend.php b/prepend.php new file mode 100644 index 0000000..b3fc4a5 --- /dev/null +++ b/prepend.php @@ -0,0 +1,20 @@ +