Repository: terran4j/commons Branch: master Commit: 863b68f3fb91 Files: 446 Total size: 2.1 MB Directory structure: gitextract_jq1qgnkm/ ├── .gitignore ├── LICENSE ├── README.md ├── build.txt ├── commons-api2doc/ │ ├── .gitignore │ ├── README.md │ ├── doc/ │ │ ├── TODO.md │ │ └── aboutCurl.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── api2doc/ │ │ │ ├── Api2DocMocker.java │ │ │ ├── annotations/ │ │ │ │ ├── Api2Doc.java │ │ │ │ ├── ApiComment.java │ │ │ │ ├── ApiError.java │ │ │ │ └── ApiErrors.java │ │ │ ├── codewriter/ │ │ │ │ ├── CodeConfig.java │ │ │ │ ├── CodeOutput.java │ │ │ │ ├── CodeUtils.java │ │ │ │ ├── EnumCodeWriter.java │ │ │ │ ├── FileCodeOutput.java │ │ │ │ ├── JavaBeanCodeWriter.java │ │ │ │ ├── MemoryCodeOutput.java │ │ │ │ └── RetrofitCodeWriter.java │ │ │ ├── config/ │ │ │ │ ├── Api2DocConfiguration.java │ │ │ │ └── EnableApi2Doc.java │ │ │ ├── controller/ │ │ │ │ ├── Api2DocController.java │ │ │ │ ├── ApiEntry.java │ │ │ │ ├── ApiInfo.java │ │ │ │ ├── ApiMetaController.java │ │ │ │ └── MenuData.java │ │ │ ├── domain/ │ │ │ │ ├── ApiDataType.java │ │ │ │ ├── ApiDocObject.java │ │ │ │ ├── ApiDocUtils.java │ │ │ │ ├── ApiErrorObject.java │ │ │ │ ├── ApiFolderObject.java │ │ │ │ ├── ApiObject.java │ │ │ │ ├── ApiParamLocation.java │ │ │ │ ├── ApiParamObject.java │ │ │ │ ├── ApiResultObject.java │ │ │ │ └── DateConverter.java │ │ │ ├── impl/ │ │ │ │ ├── Api2DocCollector.java │ │ │ │ ├── Api2DocObjectFactory.java │ │ │ │ ├── Api2DocProperties.java │ │ │ │ ├── Api2DocService.java │ │ │ │ ├── Api2DocUtils.java │ │ │ │ ├── ApiCommentUtils.java │ │ │ │ ├── ClasspathFreeMarker.java │ │ │ │ ├── CurlBuilder.java │ │ │ │ ├── DocMenuBuilder.java │ │ │ │ ├── DocPageBuilder.java │ │ │ │ ├── FlexibleString.java │ │ │ │ └── MappingMethod.java │ │ │ └── meta/ │ │ │ ├── ApiMetaService.java │ │ │ ├── ClassMeta.java │ │ │ ├── MethodMeta.java │ │ │ └── ParamMeta.java │ │ └── resources/ │ │ ├── api2doc/ │ │ │ └── welcome.md │ │ ├── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── api2doc/ │ │ │ ├── codewriter/ │ │ │ │ ├── bean.java.ftl │ │ │ │ ├── enum.java.ftl │ │ │ │ └── retrofit.java.ftl │ │ │ └── impl/ │ │ │ └── doc.md.ftl │ │ ├── static/ │ │ │ └── api2doc/ │ │ │ ├── css/ │ │ │ │ ├── doc.less │ │ │ │ ├── home.less │ │ │ │ ├── md.less │ │ │ │ └── test.less │ │ │ ├── flexible-lite/ │ │ │ │ └── flexible-lite-1.0.js │ │ │ ├── less/ │ │ │ │ └── less-1.7.0.js │ │ │ ├── test.html │ │ │ └── vue/ │ │ │ └── vue-2.5.10.js │ │ └── templates/ │ │ └── api2doc/ │ │ ├── doc.ftl │ │ └── home.ftl │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── terran4j/ │ │ ├── demo/ │ │ │ └── api2doc/ │ │ │ ├── Api2DocDemoApp.java │ │ │ ├── CodeGenerator.java │ │ │ ├── FileInfo.java │ │ │ ├── ShowMappingController.java │ │ │ ├── ShowParamController.java │ │ │ ├── ShowResultController.java │ │ │ ├── User.java │ │ │ ├── UserController.java │ │ │ ├── UserController1.java │ │ │ ├── UserController2.java │ │ │ ├── UserController3.java │ │ │ ├── UserGroup.java │ │ │ └── UserType.java │ │ └── test/ │ │ └── api2doc/ │ │ ├── Api2DocCollectorTest.java │ │ ├── Api2DocObjectFactoryTest.java │ │ ├── Api2DocUtilsTest.java │ │ ├── ApiCommentUtilsTest.java │ │ ├── ApiResultObjectTest.java │ │ ├── Application.java │ │ ├── BaseApi2DocTest.java │ │ ├── CurlBuilderTest.java │ │ ├── JavaBeanCodeWriterTest.java │ │ ├── MyBean.java │ │ ├── ParseApiCommentOnMethod.java │ │ ├── ParseApiCommentOnParam.java │ │ ├── ParseApiCommentOnSeeClass.java │ │ ├── ParseApiCommentOnSeeClassLoop.java │ │ ├── ParseEnumTest.java │ │ ├── ParseListBeanTest.java │ │ ├── ResultSourceTypeTest.java │ │ └── ToMockResultTest.java │ └── resources/ │ ├── api2doc/ │ │ ├── demo3/ │ │ │ ├── 1-项目简介.md │ │ │ ├── 11-接口1的补充说明.md │ │ │ ├── 2-技术架构.md │ │ │ ├── 21-接口2的补充说明.md │ │ │ └── 3-应用场景.md │ │ └── welcome.md │ └── application.yml ├── commons-armq/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── armq/ │ │ ├── ArmqConfig.java │ │ ├── ConsumerConfig.java │ │ ├── MessageConsumer.java │ │ ├── MessageEntity.java │ │ ├── MessageService.java │ │ └── impl/ │ │ ├── MessageConsumerTask.java │ │ ├── MessageConsumerTransfer.java │ │ └── MessageServiceImpl.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── test/ │ │ └── armq/ │ │ ├── ArmqTestApp.java │ │ ├── Normal.java │ │ ├── NormalTopicTest.java │ │ ├── Producer.java │ │ └── SendTest.java │ └── resources/ │ └── .gitignore ├── commons-dsql/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── dsql/ │ │ ├── DsqlExecutor.java │ │ ├── DsqlModifying.java │ │ ├── DsqlQuery.java │ │ ├── DsqlRepository.java │ │ ├── EnableDsqlRepositories.java │ │ ├── QueryBean.java │ │ ├── config/ │ │ │ └── DsqlConfiguration.java │ │ └── impl/ │ │ ├── CompositeBeanRowMapper.java │ │ ├── DsqlBuilder.java │ │ ├── DsqlExecutorImpl.java │ │ ├── DsqlRepositoryBeanFactory.java │ │ ├── DsqlRepositoryConfigRegistrar.java │ │ ├── DsqlRepositoryProxy.java │ │ └── SqlInfo.java │ └── test/ │ └── java/ │ └── com/ │ └── terran4j/ │ ├── demo/ │ │ └── dsql/ │ │ ├── Address.java │ │ ├── AddressDAO.java │ │ ├── AddressDistance.java │ │ ├── AddressDistanceDAO.java │ │ ├── AddressQuery.java │ │ ├── address-count.sql.ftl │ │ ├── address-delete-nearest.sql.ftl │ │ ├── address-list.sql.ftl │ │ ├── address-nearest-2.sql.ftl │ │ ├── address-nearest.sql.ftl │ │ ├── address-update-nearest.sql.ftl │ │ ├── appdsql/ │ │ │ └── DsqlDemoApplication.java │ │ └── appjpa/ │ │ └── JpaDemoApplication.java │ └── test/ │ └── dsql/ │ ├── BaseDsqlTest.java │ ├── DsqlAutoTest.java │ ├── DsqlModifyingTest.java │ ├── DsqlQueryTest.java │ ├── dao/ │ │ ├── Location.java │ │ ├── LocationDAO.java │ │ └── LocationQuery.java │ ├── dao1/ │ │ ├── LocationDsqlDAO.java │ │ ├── delete-nearest.sql.ftl │ │ └── update-nearest.sql.ftl │ ├── dao2/ │ │ ├── LocationDistance.java │ │ ├── LocationDistanceDAO.java │ │ ├── location-nearest.sql.ftl │ │ └── locations.sql.ftl │ ├── dao3/ │ │ ├── DistancedLocation.java │ │ ├── DistancedLocationDAO.java │ │ ├── location-count.sql.ftl │ │ └── locations.sql.ftl │ └── dao4/ │ ├── LocationAutoDAO.java │ ├── countLocation.sql.ftl │ ├── getLocations.sql.ftl │ └── getNearestLocation.sql.ftl ├── commons-hedis/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ ├── commons/ │ │ │ └── hedis/ │ │ │ ├── cache/ │ │ │ │ ├── CacheService.java │ │ │ │ ├── JedisCacheService.java │ │ │ │ └── RedisTemplateCacheService.java │ │ │ ├── config/ │ │ │ │ ├── EnableHedis.java │ │ │ │ └── HedisConfiguration.java │ │ │ ├── dschedule/ │ │ │ │ ├── DScheduling.java │ │ │ │ ├── DSchedulingAspect.java │ │ │ │ ├── JobExeInfo.java │ │ │ │ └── ScheduleContext.java │ │ │ └── dsyn/ │ │ │ ├── DSynchArgs.java │ │ │ ├── DSynchronized.java │ │ │ ├── DSynchronizedAspect.java │ │ │ └── Server.java │ │ └── mock/ │ │ └── hedis/ │ │ ├── MockCacheService.java │ │ └── MockHedisConfig.java │ └── test/ │ └── java/ │ └── com/ │ └── terran4j/ │ ├── demo/ │ │ └── hedis/ │ │ ├── CountService.java │ │ ├── DSynchronizedCountService.java │ │ ├── DemoCacheService.java │ │ ├── HedisDemoApp.java │ │ ├── LoopIncrementJob.java │ │ └── User.java │ └── test/ │ └── hedis/ │ ├── BaseSpringBootTest.java │ ├── BaseTestExecutionListener.java │ ├── CacheAnnoTest.java │ ├── JedisCacheServiceTest.java │ ├── MockitoInitializer.java │ ├── RedisTestConfig.java │ ├── RedissonClientTest.java │ ├── SchedulingApplication.java │ └── dsyn/ │ ├── BaseCacheTest.java │ ├── CacheTestApplication.java │ ├── CountService.java │ ├── DSynchronizedService.java │ ├── DSynchronizedTest.java │ ├── Home.java │ └── HomeService.java ├── commons-hi/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── hi/ │ │ ├── Action.java │ │ ├── ApacheHttpClientBuilder.java │ │ ├── Api2DocSupport.java │ │ ├── AssertResponseFailedException.java │ │ ├── HttpClient.java │ │ ├── HttpClientListener.java │ │ ├── HttpErrorCode.java │ │ ├── HttpErrorCode.properties │ │ ├── HttpException.java │ │ ├── HttpRequest.java │ │ ├── Param.java │ │ ├── Request.java │ │ ├── Response.java │ │ ├── Session.java │ │ ├── WrappedResponse.java │ │ ├── Write.java │ │ └── WriteTo.java │ └── test/ │ └── java/ │ ├── application.yml │ ├── com/ │ │ └── terran4j/ │ │ ├── demo/ │ │ │ └── hi/ │ │ │ ├── Calculator.java │ │ │ ├── CalculatorController.java │ │ │ ├── HttpClientApp.java │ │ │ └── demo.json │ │ └── test/ │ │ └── hi/ │ │ ├── BaseHiTest.java │ │ ├── HttpClientTest.java │ │ ├── HttpClientTest.json │ │ ├── TestHiApp.java │ │ ├── api2doc/ │ │ │ ├── Api2DocApp.java │ │ │ ├── Api2DocDemoController.java │ │ │ ├── Api2DocSupportTest.java │ │ │ └── MultiplyObject.java │ │ └── exe/ │ │ ├── ExeController.java │ │ ├── ExeTest.java │ │ ├── ExeTest.json │ │ └── PlusObject.java │ ├── example.json │ ├── hi.json │ └── http.config.json ├── commons-jfinger/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ ├── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── jfinger/ │ │ │ ├── Command.java │ │ │ ├── CommandDefine.java │ │ │ ├── CommandErrorCode.java │ │ │ ├── CommandException.java │ │ │ ├── CommandExecutor.java │ │ │ ├── CommandGroup.java │ │ │ ├── CommandGroupDefine.java │ │ │ ├── CommandGroups.java │ │ │ ├── CommandInterpreter.java │ │ │ ├── CommandOption.java │ │ │ ├── CommandOptionDefine.java │ │ │ ├── CommandOptionType.java │ │ │ ├── EnableJFinger.java │ │ │ ├── Encoding.java │ │ │ ├── JFingerConfiguration.java │ │ │ ├── OptionType.java │ │ │ ├── builtin/ │ │ │ │ ├── LogCommand.java │ │ │ │ ├── SpringCommand.java │ │ │ │ └── SystemCommand.java │ │ │ └── impl/ │ │ │ ├── BackCommandException.java │ │ │ ├── CommandInterpreterImpl.java │ │ │ ├── CommandLineApplicationListener.java │ │ │ ├── CommandLineService.java │ │ │ ├── CommandLineTask.java │ │ │ ├── DynMethodCommandExecutor.java │ │ │ └── Util.java │ │ └── error.properties │ └── test/ │ └── java/ │ ├── cmd.txt │ └── com/ │ └── terran4j/ │ └── test/ │ └── commons/ │ └── jfinger/ │ └── JFingerTestApplication.java ├── commons-reflux/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── reflux/ │ │ ├── EnableReflux.java │ │ ├── Message.java │ │ ├── OnMessage.java │ │ ├── RefluxClient.java │ │ ├── RefluxErrorCode.java │ │ ├── RefluxServer.java │ │ ├── Reply.java │ │ ├── client/ │ │ │ ├── ClientConnection.java │ │ │ ├── EnableRefluxClient.java │ │ │ ├── MessageHandler.java │ │ │ ├── RefluxClientConfiguration.java │ │ │ └── RefluxClientImpl.java │ │ └── server/ │ │ ├── ClientAppInfo.java │ │ ├── ClientConnectionInfo.java │ │ ├── EnableRefluxServer.java │ │ ├── RefluxServerConfiguration.java │ │ ├── RefluxServerEndpoint.java │ │ └── RefluxServerImpl.java │ └── test/ │ └── java/ │ └── com/ │ └── terran4j/ │ └── test/ │ └── commons/ │ └── reflux/ │ ├── Hello.java │ ├── RefluxApplication.java │ ├── SendAndReceiveTest.java │ └── TestServerEndpoint.java ├── commons-restpack/ │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── restpack/ │ │ │ ├── EnableRestPack.java │ │ │ ├── HttpResult.java │ │ │ ├── HttpResultConverter.java │ │ │ ├── Log.java │ │ │ ├── LogItem.java │ │ │ ├── PageResult.java │ │ │ ├── RestPackController.java │ │ │ ├── RestPackIgnore.java │ │ │ ├── ServletUtils.java │ │ │ ├── config/ │ │ │ │ └── RestPackConfiguration.java │ │ │ ├── impl/ │ │ │ │ ├── DateConverter.java │ │ │ │ ├── ExceptionHolder.java │ │ │ │ ├── HttpAroundHandler.java │ │ │ │ ├── HttpErrorHandler.java │ │ │ │ ├── HttpResultMapper.java │ │ │ │ ├── RestPackAdvice.java │ │ │ │ ├── RestPackAspect.java │ │ │ │ ├── RestPackConfig.java │ │ │ │ ├── RestPackMessageConverter.java │ │ │ │ └── RestPackUtils.java │ │ │ └── log/ │ │ │ ├── RestPackLogAppender.java │ │ │ └── RestPackLogAspect.java │ │ └── resources/ │ │ └── restpack/ │ │ └── freemarker.properties │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── terran4j/ │ │ ├── demo/ │ │ │ └── restpack/ │ │ │ ├── DemoHttpResultConverter.java │ │ │ ├── HelloBean.java │ │ │ ├── HelloController.java │ │ │ ├── RestPackDemoApp.java │ │ │ ├── RestPackDemoAspect.java │ │ │ ├── RestPackDemoController.java │ │ │ └── RestPackErrorController.java │ │ └── test/ │ │ └── restpack/ │ │ ├── HttpResultConverterTest.java │ │ ├── HttpResultMapperTest.java │ │ ├── NoHttpResultConverterTest.java │ │ └── RestPackTest.java │ └── resources/ │ ├── application.yml │ ├── logback.xml │ ├── static/ │ │ └── restpack/ │ │ └── hello.css │ └── templates/ │ └── restpack/ │ └── hello.ftl ├── commons-test/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── terran4j/ │ │ └── commons/ │ │ └── test/ │ │ ├── BaseHttpTest.java │ │ ├── BaseSpringBootTest.java │ │ ├── BaseTestExecutionListener.java │ │ ├── DatabaseInitializer.java │ │ ├── DatabaseTestConfig.java │ │ ├── ExtAssert.java │ │ ├── MockitoInitializer.java │ │ ├── RedisTestConfig.java │ │ ├── SigListener.java │ │ ├── TruncateTable.java │ │ ├── database.properties │ │ └── redis.properties │ └── test/ │ └── java/ │ └── com/ │ └── terran4j/ │ └── commons/ │ └── test/ │ ├── HelloService.java │ ├── MockitoTest.java │ └── TestApp.java ├── commons-util/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ ├── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── util/ │ │ │ ├── Arrays.java │ │ │ ├── Beans.java │ │ │ ├── Checker.java │ │ │ ├── Classes.java │ │ │ ├── DateTimes.java │ │ │ ├── Encoding.java │ │ │ ├── Enums.java │ │ │ ├── Expressions.java │ │ │ ├── Files.java │ │ │ ├── IOUtils.java │ │ │ ├── IdWorker.java │ │ │ ├── Jsons.java │ │ │ ├── Maths.java │ │ │ ├── Objects.java │ │ │ ├── Randoms.java │ │ │ ├── Server.java │ │ │ ├── Strings.java │ │ │ ├── config/ │ │ │ │ ├── ConfigElement.java │ │ │ │ ├── JsonConfigElement.java │ │ │ │ └── XmlConfigElement.java │ │ │ ├── error/ │ │ │ │ ├── AuthorizedException.java │ │ │ │ ├── BusinessException.java │ │ │ │ ├── CommonErrorCode.java │ │ │ │ ├── ErrorCode.java │ │ │ │ ├── ErrorCodes.java │ │ │ │ ├── ErrorMessage.java │ │ │ │ ├── ErrorReport.java │ │ │ │ ├── ResourceErrorCode.java │ │ │ │ ├── SimpleErrorCode.java │ │ │ │ └── null.properties │ │ │ ├── reflect/ │ │ │ │ └── InterfaceFilter.java │ │ │ ├── security/ │ │ │ │ ├── AsymmetricKeys.java │ │ │ │ ├── MD5Util.java │ │ │ │ └── Security.java │ │ │ ├── task/ │ │ │ │ └── LoopExecuteTask.java │ │ │ ├── value/ │ │ │ │ ├── JsonValueSource.java │ │ │ │ ├── KeyedList.java │ │ │ │ ├── MapValueSource.java │ │ │ │ ├── ResourceBundlesProperties.java │ │ │ │ ├── RichProperties.java │ │ │ │ ├── ValueSource.java │ │ │ │ ├── ValueSources.java │ │ │ │ └── ValueWrapper.java │ │ │ └── web/ │ │ │ ├── Cookies.java │ │ │ └── IPAddresses.java │ │ └── error.properties │ └── test/ │ └── java/ │ ├── com/ │ │ └── terran4j/ │ │ └── common/ │ │ └── util/ │ │ ├── ClassesTest.java │ │ ├── DateTimesTest.java │ │ ├── EnumsTest.java │ │ ├── ExpressionsTest.java │ │ ├── JsonConfigElementTest.java │ │ ├── JsonConfigElementTest.json │ │ ├── JsonsTest.java │ │ ├── LoopExecuteTaskTest.java │ │ ├── SecurityTest.java │ │ ├── StringsTest.java │ │ ├── ValueTest.java │ │ └── error/ │ │ ├── BusinessExceptionTest.java │ │ ├── MockErrorCode.java │ │ ├── MockException.properties │ │ └── testGetReport.txt │ └── error.properties ├── commons-website/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── terran4j/ │ │ │ └── commons/ │ │ │ └── website/ │ │ │ ├── config/ │ │ │ │ ├── Readme.java │ │ │ │ └── WebsiteConfiguration.java │ │ │ └── controller/ │ │ │ └── WelcomeController.java │ │ └── resources/ │ │ └── static/ │ │ └── website/ │ │ ├── flexible-lite/ │ │ │ └── flexible-lite-1.0.js │ │ ├── less/ │ │ │ └── less-1.7.0.js │ │ └── vue/ │ │ └── vue-2.5.10.js │ └── test/ │ └── java/ │ └── com/ │ └── terran4j/ │ └── test/ │ └── website/ │ ├── HelloController.java │ └── MainApp.java ├── pom.xml └── version.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target/ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .idea/ *.iml ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ ## 项目简介 本项目采用 SpringBoot 框架构建,致力于为 SpringBoot 应用程序的开发提升开发效率及编程体验。 本项目由多个子项目组成,每个子项目聚焦解决一个问题。 这里先简单介绍下这些子项目: * [commons-api2doc](https://github.com/terran4j/commons/tree/master/commons-api2doc): 简称 Api2Doc,是一款 Http API 文档自动化生成工具, 它通过反射的方式,读取 Controller 类的信息, 然后自动生成易于阅读的在线 API 文档,节省开发者手工编写 API 文档的工作量。 * [commons-restpack](https://github.com/terran4j/commons/tree/master/commons-restpack): 简称 RestPack,是一款 Http API 数据包装框架, 它可以将 Http API 的返回结果包装成统一的报文格式。 * [commons-dsql](https://github.com/terran4j/commons/tree/master/commons-dsql): 简称 DSQL,是一款从 SQL 到对象的自动映射框架,它尤其擅长动态复杂 SQL 的处理。 它结合了现在两大主流持久层框架 JPA 及 MyBatis 的优点, 从而更进一步的提高了持久层的开发效率。 * [commons-hedis](https://github.com/terran4j/commons/tree/master/commons-hedis): 简称 Hedis,是 Happy for using Redis 之意,目标是让 Redis 使用起来更容易。 Hedis 集成了 spring-data-redis, Jedis,Redisson 等 Redis 客户端框架, 并用它们解决一些具体的问题,如:分布式同步、轻量级分布式定时调度等。 ## 适用用户 适合有 Java / Kotlin + SpringBoot 开发经验的开发者们使用。 如果您有 Java 开发经验但对Spring Boot 还不熟悉的话,建议先阅读笔者写过的一本书 [《Spring Boot 快速入门》](http://www.jianshu.com/nb/14688855?order_by=seq)。 这本书的目标是帮助有 Java 开发经验的程序员们快速掌握 Spring Boot 开发技巧, 感受到 Spring Boot 的极简开发风格及超爽编程体验。 ## 软件版本 本项目中所用到的基础软件,均基于以下版本构建: * Java: 1.8 * Maven: 3.3.9 * SpringBoot: 1.5.9.RELEASE 本项目及所有子项目均在以上版本测试过,可以正常运行。 其它版本理论上相同,应该没啥区别,若遇到问题,欢迎反馈! ================================================ FILE: build.txt ================================================ ## 统一修改版本号 mvn versions:set "-DnewVersion=1.0.2-SNAPSHOT" mvn versions:commit ## 当版本为 SNAPSHOT 时的测试: mvn clean test ## 当版本为 SNAPSHOT 时的部署: mvn clean deploy -DskipTests ## 本地构建打包。 mvn clean install -DskipTests ## 所有项目一起部署太慢了,容易失败,所有分开部署: ## 建议在早上 10:00 以后执行(这会美国是晚上,网络稍好点) mvn clean deploy -DskipTests -P release -pl commons-util -am // 03:33 mvn clean deploy -DskipTests -P release -pl commons-website // 02:27 mvn clean deploy -DskipTests -P release -pl commons-restpack // 02:11 mvn clean deploy -DskipTests -P release -pl commons-api2doc // 12:58 mvn clean deploy -DskipTests -P release -pl commons-hedis // 03:05 mvn clean deploy -DskipTests -P release -pl commons-hi // 03:12 mvn clean deploy -DskipTests -P release -pl commons-test // 02:00 mvn clean deploy -DskipTests -P release -pl commons-dsql // 03:14 ================================================ FILE: commons-api2doc/.gitignore ================================================ /target/ /.settings/ /.classpath /.project *.iml ================================================ FILE: commons-api2doc/README.md ================================================ 本文介绍一个非常好用的自动化生成 Restful API 文档的工具——Api2Doc 它基于 SpringBoot ,原理类似于 Swagger2,但比 Swagger2 要简单好用。 此项目已经放到 github 中,需要源码的朋友请点击 [这里](https://github.com/terran4j/commons/tree/master/commons-api2doc) ## 目录 * 项目背景 * Api2Doc 简介 * 引入 Api2Doc 依赖 * 启用 Api2Doc 服务 * 给 Controller 类上添加文档注解 * @Api2Doc 注解详述 * @ApiComment 注解详述 * @ApiError 注解详述 * 给文档菜单项排序 * 补充自定义文档 * 定制文档的欢迎页 * 定制文档的标题及图标 * 关闭 Api2Doc 服务 * 后续开发计划 ## 项目背景 在互联网/移动互联网软件的研发过程中,大多数研发团队前后台分工是非常明确的, 后台工程师负责服务端系统的开发,一般是提供 HTTP/HTTPS 的 Restful API 接口, 前端工程师则负责 Android、iOS、H5页面的开发,需要调用 Restful API 接口。 这就需要有一套 Restful API 文档,以帮助两方在 API 接口进行沟通,并达成一致意见。 一般情况下,编写文档的工作都会落在后台工程师身上,毕竟 API 是他们提供的嘛。 但问题是,编写 Restful API 文档是一件既繁琐、又费时、还对提高技术能力没啥帮助的苦差事, 尤其在是快速迭代、需求频繁修改的项目中,改了代码还要同步改文档, 哪点改错了或改漏了都可能产生前后端实现的不一致,导致联调时发现 BUG, 这个锅最终还是要后台工程师来背(宝宝心里苦啊...)。 因此,业界就出现了一些**根据代码自动生成 Restful API 文档**的开源项目, 与 Spring Boot 结合比较好的是 Swagger2,Swagger2 通过读取 Controller 代码中的注解信息,来自动生成 API 文档,可以节省大量的手工编写文档的工作量。 本项目作者之前也是用的 Swagger2,但发现 Swagger2 也有好多地方用得不爽: 第一,**Swagger2 的注解非常臃肿**,我们看下这段代码: ```java @RestController @RequestMapping(value = "/user3") public class UserController2Swagger2 { @ApiOperation(value = "获取指定id用户详细信息", notes = "根据user的id来获取用户详细信息", httpMethod = "GET") @ApiImplicitParams({ @ApiImplicitParam(name = "userName", value = "用户名", paramType = "query", required = true, dataType = "String"), @ApiImplicitParam(name = "password", value = "用户密码", paramType = "query", required = true, dataType = "String") }) @RequestMapping(name = "用户注册", value = "/regist", method = RequestMethod.GET) public UserInfo regist(@RequestParam("userName") String userName, @RequestParam("password") String password) { return new UserInfo(); } } ``` @ApiOperation、@ApiImplicitParam 都是 Swagger2 提供的注解,用于定义 API 信息。 其实,API 方法本身就包含了很多信息,如HTTP Method、参数名、参数类型等等, 像 @ApiImplicitParam 中除了 value 属性有用外,其它都是重复描述。 第二,Swagger2 的页面排版不太友好,它是一个垂直排列的方式,不利于信息的展示。 并且看 API 详细信息还要一个个展开,中间还夹杂着测试的功能,反正作为文档是不易于阅读; 至于作为测试工具嘛...,现在专业的测试工具也有很多,测试人员好像也不选它。 第三,Swagger2 还有好多细节没做好,比如看这个图: ![swgger2-1.png](http://upload-images.jianshu.io/upload_images/4489584-575b3f94d746d921.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 红框中的 API 其实对应的是同一个方法,之所以有这么多,只是因为写这个方法 时没有指定 method: ```java @RestController @RequestMapping(value = "/user2") public class UserController2Swagger2 { @RequestMapping(value = "/do_something") public void doSomethingRequiredLogon() { } // 其它方法,这里省略... } ``` (当没指定 method 时,Spring Boot 会默认让这个接口支持所有的 method) 因此,考虑到与其长长久久忍受 Swagger2 的各种不爽,不如花些时间做一个 更好用的“自动化文档系统”,于是就诞生了本项目: Api2Doc 。 ## Api2Doc 简介 Api2Doc 专注于 Restful API 文档的自动生成,它的原理与 Swagger2 是类似的, 都是通过反射,分析 Controller 中的信息生成文档,但它要比 Swagger2 好很多。 最大的不同是: **Api2Doc 比 Swagger2 要少写很多代码**。 举个例子,使用 Swagger2 的代码是这样的: ```java @RestController @RequestMapping(value = "/user") public class UserController { @ApiOperation(value = "添加用户", httpMethod = "POST", notes = "向用户组中添加用户,可以指定用户的类型") @ApiImplicitParams({ @ApiImplicitParam(name = "group", value = "用户组名", paramType = "query", required = true, dataType = "String"), @ApiImplicitParam(name = "name", value = "用户名", paramType = "query", required = true, dataType = "String"), @ApiImplicitParam(name = "type", value = "用户类型", paramType = "query", required = true, dataType = "String") }) @RequestMapping(value = "/addUser", method = RequestMethod.POST) public User addUser(String group, String name, String type) { return null; // TODO: 还未实现。 } } ``` 我们看下使用 Api2Doc 注解修饰后的代码: ```java @Api2Doc(id = "users") @ApiComment(seeClass = User.class) @RestController @RequestMapping(value = "/api2doc/demo2") public class UserController2 { @ApiComment("添加一个新的用户。") @RequestMapping(name = "新增用户", value = "/user", method = RequestMethod.POST) public User addUser(String group, String name, String type) { return null; // TODO: 还未实现。 } // 其它方法,这里省略... } ``` 看,Api2Doc 仅需要在方法上加上 @Api2Doc @ApiComment 注解等极少数代码, 但它生成的文档可一点不含糊,如下图所示: ![api2doc-2-1.png](http://upload-images.jianshu.io/upload_images/4489584-98f94cb360c0ccde.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![api2doc-2-2.png](http://upload-images.jianshu.io/upload_images/4489584-fedf2897f5c217b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 有的朋友可能会觉得很奇怪:文档页面上的说明、示例值等内容,在代码中没有写啊, 这些是哪来的呢? 这里涉及到 Api2Doc 的核心设计理念,就是:它尽可能通过智能分析,自动收集 生成文档所需的信息,从而**让用户少写代码**。 说得有点抽象哈,下面我们来正面回答这个问题,请大家注意这个类上有一个注解: ``` @ApiComment(seeClass = User.class) ``` 它意思是: 在 API 方法上遇到没写说明信息时,请参照 User 类中的定义的说明信息。 下面是 User 类的代码: ```java public class User { @ApiComment(value = "用户id", sample = "123") private Long id; @ApiComment(value = "用户名", sample = "terran4j") private String name; @ApiComment(value = "账号密码", sample = "sdfi23skvs") private String password; @ApiComment(value = "用户所在的组", sample = "研发组") private String group; @ApiComment(value = "用户类型", sample = "admin") private UserType type; @ApiComment(value = "是否已删除", sample = "true") @RestPackIgnore private Boolean deleted; @ApiComment(value = "创建时间\n也是注册时间。") private Date createTime; // 省略 getter / setter 方法。 } ``` 大家看明白了没? API 方法中的参数,如果与 User 类的属性同名的话,就用类 属性的 @ApiComment 说明信息自动填充。 其实这也符合实际的业务逻辑。因为在大部分项目中,有的字段会在多个实体类、 多个 API 方法中用到,完全没有必要重复编写其说明信息,只要有一个地方定义好了, 然后其它地方参照就行了。 当然,这只是 Api2Doc 比 Swagger2 好用的特性之一,还有不少比 Swagger2 好用的地方。 下面我们就来全面讲解它的用法,希望可以帮助开发者们从文档编写的苦海中解脱出来。 ## 引入 Api2Doc 依赖 如果是 maven ,请在 pom.xml 中添加依赖,如下所示: ```xml com.github.terran4j terran4j-commons-api2doc ${api2doc.version} ``` 如果是 gradle,请在 build.gradle 中添加依赖,如下所示: ```groovy compile "com.github.terran4j:terran4j-commons-api2doc:${api2doc.version}" ``` ${api2doc.version} **最新稳定版,请参考 [这里](https://github.com/terran4j/commons/blob/master/version.md)** ## 启用 Api2Doc 服务 本教程的示例代码在 src/test/java 目录的 com.terran4j.demo.api2doc 中, 您也可以从 [这里](https://github.com/terran4j/commons/tree/master/commons-api2doc/src/test/java/com/terran4j/demo/api2doc) 获取到。 首先,我们需要在有 @SpringBootApplication 注解的类上,添加 @EnableApi2Doc 注解,以启用 Api2Doc 服务,如下代码所示: ```java package com.terran4j.demo.api2doc; import com.terran4j.commons.api2doc.config.EnableApi2Doc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // 文档访问地址: http://localhost:8080/api2doc/home.html @EnableApi2Doc @SpringBootApplication public class Api2DocDemoApp { public static void main(String[] args) { SpringApplication.run(Api2DocDemoApp.class, args); } } ``` ## 给 Controller 类上添加文档注解 然后我们在 RestController 类添加 @Api2Doc 注解,在需要有文档说明的地方 添加 @ApiComment 注解即可,如下所示: ```java package com.terran4j.demo.api2doc; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.api2doc.annotations.ApiComment; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @Api2Doc(id = "demo1", name = "用户接口1") @ApiComment(seeClass = User.class) @RestController @RequestMapping(value = "/api2doc/demo1") public class UserController1 { @ApiComment("添加一个新的用户。") @RequestMapping(name = "新增用户", value = "/user", method = RequestMethod.POST) public User addUser(String group, String name, @ApiComment("用户类型") UserType type) { return null; // TODO: 还未实现。 } } ``` 这个方法的返回类型 User 类的定义为: ```java public class User { @ApiComment(value = "用户id", sample = "123") private Long id; @ApiComment(value = "用户名", sample = "terran4j") private String name; @ApiComment(value = "账号密码", sample = "sdfi23skvs") private String password; @ApiComment(value = "用户所在的组", sample = "研发组") private String group; @ApiComment(value = "用户类型", sample = "admin") private UserType type; @ApiComment(value = "是否已删除", sample = "true") @RestPackIgnore private Boolean deleted; @ApiComment(value = "创建时间\n也是注册时间。") private Date createTime; // 省略 getter / setter 方法。 } ``` 以及 type 属性的类型,也就是 UserType 类的定义为: ```java package com.terran4j.demo.api2doc; import com.terran4j.commons.api2doc.annotations.ApiComment; public enum UserType { @ApiComment("管理员") admin, @ApiComment("普通用户") user } ``` 编写好代码后,我们运行 main 函数,访问 Api2Doc 的主页面: ``` http://localhost:8080/api2doc/home.html ``` 文档页面如下: ![api2doc-3-1.png](http://upload-images.jianshu.io/upload_images/4489584-ef8f5a56917da47f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![api2doc-3-2.png](http://upload-images.jianshu.io/upload_images/4489584-55ea3a295a009855.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 说明 Api2Doc 服务起作用了,就是这么简单! ## @Api2Doc 注解详述 Api2Doc 一共有 3 个注解:@Api2Doc、@ApiComment 及 @ApiError 。 @Api2Doc 用于对文档的生成进行控制。 @Api2Doc 修饰在类上,表示这个类会参与到文档生成过程中,Api2Doc 服务 会扫描 Spring 容器中所有的 Controller 类,只有类上有 @Api2Doc 的类, 才会被生成文档,一个类对应于文档页面左侧的一级菜单项,@Api2Doc 的 name 属性则表示这个菜单项的名称。 @Api2Doc 也可以修饰在方法,不过在方法上的 @Api2Doc 通常是可以省略, Api2Doc 服务会扫描这个类的所有带有 @RequestMapping 的方法, 每个这样的方法对应文档页面的左侧的二级菜单项, 菜单项的名称取 @RequestMapping 的 name 属性,当然您仍然可以在方法上用 @Api2Doc 的 name 属性进行重定义。 ## @ApiComment 注解详述 @ApiComment 用于对 API 进行说明,它可以修饰在很多地方: * 修饰在类上,表示对这组 API 接口进行说明; * 修饰在方法上,表示对这个 API 接口进行说明; * 修饰在参数上,表示对这个 API 接口的请求参数进行说明; * 修饰在返回类型的属性上,表示对这个 API 接口的返回字段进行说明; * 修饰在枚举项上,表示对枚举项进行说明; 如果相同名称、相同意义的属性或参数字段,其说明已经在别的地方定义过了, 可以用 @ApiComment 的 seeClass 属性表示采用指定类的同名字段上的说明信息, 所以如这段代码: ```java @Api2Doc(id = "demo1", name = "用户接口1") @ApiComment(seeClass = User.class) @RestController @RequestMapping(value = "/api2doc/demo1") public class UserController1 { @ApiComment("添加一个新的用户。") @RequestMapping(name = "新增用户", value = "/user", method = RequestMethod.POST) public User addUser(String group, String name, UserType type) { return null; // TODO: 还未实现。 } } ``` 虽然 group, name ,type 三个参数没有用 @ApiComment 进行说明, 但由于这个类上有 @ApiComment(seeClass = User.class) , 因此只要 User 类中有 group, name ,type 字段并且有 @ApiComment 的说明就行了。 ## @ApiError 注解详述 @ApiError 用于定义错误码,有的 API 方法在执行业务逻辑时会产生错误, 出错后会在返回报文包含错误码,以方便客户端根据错误码作进一步的处理, 因此也需要在 API 文档上体现错误码的说明。 如下代码演示了 @ApiError 的用法: ```java @Api2Doc(id = "demo", name = "用户接口", order = 0) @ApiComment(seeClass = User.class) @RestController @RequestMapping(value = "/src/test/resources/demo") public class UserController { @Api2Doc(order = 50) @ApiComment("根据用户id,删除指定的用户") @ApiError(value = "user.not.found", comment = "此用户不存在!") @ApiError(value = "admin.cant.delete", comment = "不允许删除管理员用户!") @RequestMapping(name = "删除指定用户", value = "/user/{id}", method = RequestMethod.DELETE) public void delete(@PathVariable("id") Long id) { } } ``` @ApiError 的 value 属性表示错误码,comment 表示错误码的说明。 错误码信息会显示在文档的最后面,效果如下所示: ![api2doc-7.png](http://upload-images.jianshu.io/upload_images/4489584-44a77bbb3c1e84da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 给文档菜单项排序 我们可以用 @Api2Doc 中的 order 属性给菜单项排序,order 的值越小, 该菜单项就越排在前面,比如对于这段代码: ```java package com.terran4j.demo.api2doc; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.api2doc.annotations.ApiComment; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.List; @Api2Doc(id = "demo2", name = "用户接口2", order = 1) @ApiComment(seeClass = User.class) @RestController @RequestMapping(value = "/api2doc/demo2") public class UserController2 { @Api2Doc(order = 10) @ApiComment("添加一个新的用户。") @RequestMapping(name = "新增用户", value = "/user", method = RequestMethod.POST) public User addUser( @ApiComment("用户组名称") String group, @ApiComment("用户名称") String name, @ApiComment("用户类型") UserType type) { return null; // TODO: 还未实现。 } @Api2Doc(order = 20) @ApiComment("根据用户id,查询此用户的信息") @RequestMapping(name = "查询单个用户", value = "/user/{id}", method = RequestMethod.GET) public User getUser(@PathVariable("id") Long id) { return null; // TODO: 还未实现。 } @Api2Doc(order = 30) @ApiComment("查询所有用户,按注册时间进行排序。") @RequestMapping(name = "查询用户列表", value = "/users", method = RequestMethod.GET) public List getUsers() { return null; // TODO: 还未实现。 } @Api2Doc(order = 40) @ApiComment("根据指定的组名称,查询该组中的所有用户信息。") @RequestMapping(name = "查询用户组", value = "/group/{group}", method = RequestMethod.GET) public UserGroup getGroup(@PathVariable("group") String group) { return null; // TODO: 还未实现。 } } ``` 显示的结果为: ![api2doc-3.png](http://upload-images.jianshu.io/upload_images/4489584-0818fdef543c8c07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 在类上的 @Api2Doc 同样可以给一级菜单排序,规则是一样的,这里就不演示了。 ## 补充自定义文档 有时候光有自动生成的 API 文档似乎还不太完美,或许我们想补充点别的什么东西, 比如: 对项目的背景介绍、技术架构说明之类,那这个要怎么弄呢? Api2Doc 允许用 md 语法手工编写文档,并集成到自动生成的 API 文档之中,方法如下: 首先,要在类上的 @Api2Doc 定义 id 属性,比如对下面这个类: ```java package com.terran4j.demo.api2doc; import com.terran4j.commons.api2doc.annotations.Api2Doc; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Api2Doc(id = "demo3", name = "用户接口3") @RestController @RequestMapping(value = "/api2doc/demo3") public class UserController3 { @Api2Doc(order = 10) @RequestMapping(name = "接口1", value = "/m1") public void m1() { } @Api2Doc(order = 20) @RequestMapping(name = "接口2", value = "/m2") public void m2() { } } ``` @Api2Doc(id = "demo3", name = "用户接口3") 表示:对应的一级菜单“用户接口3” 的 id 为 demo3。 然后,我们在 src/main/resources 中创建目录 api2doc/demo3, 前面的 api2doc 是固定的,后面的 demo3 表示这个目录中的文档是添加到 id 为 demo3 的一级文档菜单下。 然后我们在 api2doc/demo3 目录中编写 md 格式的文档,如下图所示: ![api2doc-4.png](http://upload-images.jianshu.io/upload_images/4489584-a76a84061f2771d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 文件名的格式为 ${order}-${文档名称}.md,即 - 号前面的数字表示这个文档的排序, 与 @Api2Doc 中的 order 属性是一样的,而 - 号后面是文档名称,也就是二级菜单的名称。 因此,最后文档的显示效果为: ![api2doc-5.png](http://upload-images.jianshu.io/upload_images/4489584-73814ce5bde91b2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 看,手工编写的补充文档与自动生成的 API 文档,通过 order 进行排序组合在一起, 看起来毫无违和感。 ## 定制文档的欢迎页 每次访问文档页面 http://localhost:8080/api2doc/home.html 时, 中间的内容是非常简单的一句: ``` 欢迎使用 Api2Doc ! ``` 这似乎有点不太好,不过没关系,我们可以编写自己的欢迎页。 方法很简单,在 src/main/resources 目录的 api2doc 目录下,创建一个名为 welcome.md 的文件(这个名称是固定的),然后用 md 语法编写内容就可以。 ## 配置文档的标题及图标 可以在 application.yml 中配置文档的标题及图标,如下所示: ```yaml api2doc: title: Api2Doc示例项目——接口文档 icon: https://spring.io/img/homepage/icon-spring-framework.svg ``` 图标为一个全路径 URL,或本站点相对路径 URL 都行。 配置后的显示效果为: ![api2doc-6.png](http://upload-images.jianshu.io/upload_images/4489584-494a0c8042aaffb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 关闭 Api2Doc 服务 您在 application.yml 中配置 api2doc.enabled 属性,以开启或关闭 Api2Doc 服务,如: ```yaml # 本地环境 api2doc: title: Api2Doc示例项目——接口文档 icon: https://spring.io/img/homepage/icon-spring-framework.svg --- # 线上环境 spring: profiles: online api2doc: enabled: false ``` api2doc.enabled 为 false 表示关闭 Api2Doc 服务,不写或为 true 表示启用。 由于 Api2Doc 服务没有访问权限校验,建议您在受信任的网络环境(如公司内网) 中才启用 Api2Doc 服务。 ## 后续开发计划 参见[后续开发计划](https://github.com/terran4j/commons/blob/master/commons-api2doc/doc/TODO.md) ================================================ FILE: commons-api2doc/doc/TODO.md ================================================ ## 后续开发计划 Api2Doc 项目后续计划要开发的功能如下: * 参数为对象,参数类型object,可以指出具体类型 * 可以导出为 html 格式的文档文件,可以离线浏览, 支持在页面上手工操作导出,以及程序调用 API 自定义导出两种方式。 * 可以导出为 md 格式的文档文件,可以放到 md 运行环境中浏览, 支持在页面上手工操作导出,以及程序调用 API 自定义导出两种方式。 * 对测试进行支持。 * 文档样式设计得更漂亮。 * 文档样式支持自定义。 ### 1.0.2 (已发布于 2018-04-06 ) 新增了以下功能: 1. 支持各种用 @XxxMapping 修饰的方法生成文档,包括: `@GetMapping`、 `@PostMapping`、 `@PutMapping`、 `@DeleteMapping`、 `@PatchMapping`; (之前只支持 `@RequestMapping` 。) 2. 支持各种形式的参数,包括: `@PathVariable`、 `@RequestHeader`、 `@CookieValue`、 `@RequestPart`; (之前只支持 `@RequestParam` 。) 并在文档页面的“请求参数”表格,加上“参数形式”这一列。 3. 文档页面,“URL示例”改为“请求示例”, 请求示例为 curl 命令格式,并支持所有的 HTTP 方法 , (之前是 URL 格式,并且只支持了 GET 方法)。 修复了以下 BUG: 1. 修复当返回类型为简单类型时未能显示在文档的 BUG。 ================================================ FILE: commons-api2doc/doc/aboutCurl.md ================================================ ## 什么是 curl curl 是一款很强大的 http 命令行工具,可以的向服务端发起 HTTP/HTTPS 请求, 是一个很方便 HTTP API 测试手段。 ## 如何安装 curl - 如果是 Linux / MAC 系统,一般系统都自带了 curl (如果没有安装也很简单,请自行百度)。 - 如果是 Windows 系统,建议先安装 Cygwin64 ,然后在 Cygwin64 中安装使用 curl 。 下面就介绍下如何使用 Cygwin64 包管理器安装 curl Cygwin64 包管理器下载地址: [http://www.cygwin.com/setup-x86_64.exe](http://www.cygwin.com/setup-x86_64.exe) 下载后运行,一直下一步到以下界面: ![image](http://upload-images.jianshu.io/upload_images/4489584-b4431f26c3440bd6?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) View 选择 Full,Search 后面输入:curl 然后点击第一行的Skip,点击下一步,后面就按默认的设置一步步安装即可。 ================================================ FILE: commons-api2doc/pom.xml ================================================ 4.0.0 com.github.terran4j terran4j-commons-parent 1.0.4-SNAPSHOT terran4j-commons-api2doc jar terran4j-commons-api2doc https://github.com/terran4j/commons com.vladsch.flexmark flexmark com.vladsch.flexmark flexmark-util com.vladsch.flexmark flexmark-ext-tables com.vladsch.flexmark flexmark-ext-spec-example org.springframework.boot spring-boot-starter-freemarker com.github.terran4j terran4j-commons-util com.github.terran4j terran4j-commons-website com.github.terran4j terran4j-commons-restpack org.aspectj aspectjweaver com.google.code.gson gson com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-annotations com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.module jackson-module-jsonSchema ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/Api2DocMocker.java ================================================ package com.terran4j.commons.api2doc; import com.terran4j.commons.api2doc.impl.Api2DocObjectFactory; import java.util.List; public class Api2DocMocker { public static T mockBean(Class clazz) { return Api2DocObjectFactory.createBean(clazz); } public static List mockList(Class clazz, int size) { return Api2DocObjectFactory.createList(clazz, size); } public static T[] mockArray(Class clazz, int size) { return Api2DocObjectFactory.createArray(clazz, size); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/Api2Doc.java ================================================ package com.terran4j.commons.api2doc.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) public @interface Api2Doc { int DEFAULT_ORDER = 100; /** * 文档的id。
* 对于一个类中的重载方法,一定要用不同的 id 区分,不然会出错。 * * @return */ @AliasFor("value") String id() default ""; /** * 文档的id。
* 对于一个类中的重载方法,一定要用不同的 id 区分,不然会出错。 * * @return */ @AliasFor("id") String value() default ""; /** * 是否忽略此文档。 * * @return */ boolean ignore() default false; /** * 设置文档的排序。
* 数字越小,排序越靠前。 * * @return */ int order() default DEFAULT_ORDER; String name() default ""; } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiComment.java ================================================ package com.terran4j.commons.api2doc.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, }) public @interface ApiComment { String value() default ""; String sample() default ""; Class seeClass() default Object.class; String seeField() default ""; } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiError.java ================================================ package com.terran4j.commons.api2doc.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @Repeatable(ApiErrors.class) public @interface ApiError { String value(); String comment() default ""; } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiErrors.java ================================================ package com.terran4j.commons.api2doc.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface ApiErrors { ApiError[] value(); } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeConfig.java ================================================ package com.terran4j.commons.api2doc.codewriter; import java.util.List; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiParamObject; public class CodeConfig { public List getExtraPrams(ApiDocObject doc) { return null; } private String pkgName; private String declaredComment; public String getPkgName() { return pkgName; } public void setPkgName(String pkgName) { this.pkgName = pkgName; } public String getDeclaredComment() { return declaredComment; } public void setDeclaredComment(String declareComment) { this.declaredComment = declareComment; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeOutput.java ================================================ package com.terran4j.commons.api2doc.codewriter; public interface CodeOutput { void writeCodeFile(String fileName, String fileContent); void setPercent(int percent); void log(String log, String... args); } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeUtils.java ================================================ package com.terran4j.commons.api2doc.codewriter; import java.util.Set; public class CodeUtils { public static final void addImport(Class clazz, Set imports) { if (clazz == null || clazz.getPackage() == null) { return; } String paramPkgName = clazz.getPackage().getName(); if (paramPkgName.equals("java.lang")) { return; } if (paramPkgName.startsWith("java.") || paramPkgName.startsWith("javax.")) { imports.add(clazz.getName()); } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/EnumCodeWriter.java ================================================ package com.terran4j.commons.api2doc.codewriter; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import com.terran4j.commons.api2doc.impl.Api2DocUtils; import com.terran4j.commons.api2doc.impl.ApiCommentUtils; import com.terran4j.commons.api2doc.impl.FlexibleString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.impl.ClasspathFreeMarker; import freemarker.template.Template; @Service public class EnumCodeWriter { private static final Logger log = LoggerFactory.getLogger(EnumCodeWriter.class); @Autowired private ClasspathFreeMarker classpathFreeMarker; private Template enumTemplate = null; @PostConstruct public void init() { try { enumTemplate = classpathFreeMarker.getTemplate(getClass(), // "enum.java.ftl"); } catch (Exception e) { throw new RuntimeException(e); } } @SuppressWarnings({"rawtypes", "unchecked"}) public void writeCode(Class currentClass, String className, // CodeOutput out, CodeConfig config) throws Exception { if (currentClass == null || !currentClass.isEnum()) { return; } Map model = new HashMap<>(); model.put("class", className); if (config == null) { config = new CodeConfig(); } model.put("config", config); List enumInfos = new ArrayList<>(); Class> enumClass = (Class>) currentClass; Enum[] enums = enumClass.getEnumConstants(); for (Enum enumObject : enums) { EnumInfo enumInfo = new EnumInfo(); String name = enumObject.name(); enumInfo.setName(name); String comment = null; Field field = null; try { field = enumClass.getDeclaredField(name); } catch (NoSuchFieldException | SecurityException e1) { log.error("Can't get field \"" + name + "\" from Enum: " // + enumClass.getName(), e1); continue; } ApiComment apiComment = field.getAnnotation(ApiComment.class); comment = ApiCommentUtils.getComment( apiComment, null, field.getName()); if (comment != null) { comment = new FlexibleString(comment).javadoc(1); } // if (apiComment != null && StringUtils.hasText(apiComment.value())) { // comment = new FlexibleString(apiComment.value().trim()).javadoc(1); // } enumInfo.setComment(comment); enumInfos.add(enumInfo); } model.put("enums", enumInfos); String code = classpathFreeMarker.build(enumTemplate, model); out.writeCodeFile(className + ".java", code); } public static final class EnumInfo { private String comment; private String name; public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getName() { return name; } public void setName(String name) { this.name = name; } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/FileCodeOutput.java ================================================ package com.terran4j.commons.api2doc.codewriter; import java.io.File; import com.terran4j.commons.util.Files; public class FileCodeOutput implements CodeOutput { private final String path; public FileCodeOutput(String path) { super(); this.path = path; } @Override public void writeCodeFile(String fileName, String fileContent) { File file = new File(path + "/" + fileName); Files.writeFile(fileContent, file); } @Override public void setPercent(int percent) { } @Override public void log(String log, String... args) { } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/JavaBeanCodeWriter.java ================================================ package com.terran4j.commons.api2doc.codewriter; import com.terran4j.commons.api2doc.domain.ApiDataType; import com.terran4j.commons.api2doc.domain.ApiResultObject; import com.terran4j.commons.api2doc.domain.DateConverter; import com.terran4j.commons.api2doc.impl.ClasspathFreeMarker; import freemarker.template.Template; import org.apache.commons.collections4.map.HashedMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.PostConstruct; import java.util.*; import static java.util.Locale.ENGLISH; @Service public class JavaBeanCodeWriter { private static final String GET_PREFIX = "get"; private static final String SET_PREFIX = "set"; private static final String IS_PREFIX = "is"; @Autowired private ClasspathFreeMarker classpathFreeMarker; private Template javaBeanTemplate = null; @PostConstruct public void init() { try { javaBeanTemplate = classpathFreeMarker.getTemplate(getClass(), // "bean.java.ftl"); } catch (Exception e) { throw new RuntimeException(e); } } public void writeCode(ApiResultObject result, String className, // CodeOutput out, CodeConfig config) throws Exception { List children = result.getChildren(); if (children == null || children.size() == 0) { return; } Map model = getModel(result, className, config); String code = classpathFreeMarker.build(javaBeanTemplate, model); out.writeCodeFile(className + ".java", code); } public Map getModel(ApiResultObject result, String className, // CodeConfig config) { Map model = new HashedMap<>(); model.put("class", className); if (config == null) { config = new CodeConfig(); } model.put("config", config); // java 类上的注释。 String comment = result.getComment().javadoc(0); if (StringUtils.hasText(comment)) { model.put("comment", comment); } Set imports = new HashSet<>(); model.put("imports", imports); List fields = new ArrayList(); List children = result.getChildren(); for (ApiResultObject child : children) { FieldInfo field = new FieldInfo(); String name = child.getId(); field.setName(name); String type = toTypeName(child); field.setType(type); String fieldComment = child.getComment().javadoc(1); if (StringUtils.hasText(fieldComment)) { field.setComment(fieldComment); } // Date 自动转成 Long 类型了。 Class sourceType = getSourceType(child); CodeUtils.addImport(sourceType, imports); boolean isBooleanClass = (child.getDataType() == ApiDataType.BOOLEAN); String getMethod = toGetMethodName(isBooleanClass, name); field.setGetMethod(getMethod); String setMethod = toSetMethodName(name); field.setSetMethod(setMethod); fields.add(field); } model.put("fields", fields); return model; } private String toGetMethodName(boolean isBooleanClass, String fieldName) { String baseName = getBaseName(fieldName); String methodName = null; if (isBooleanClass) { methodName = IS_PREFIX + baseName; } else { methodName = GET_PREFIX + baseName; } return methodName; } private String toSetMethodName(String fieldName) { String baseName = getBaseName(fieldName); String methodName = SET_PREFIX + baseName; return methodName; } private String getBaseName(String name) { return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1); } private Class getSourceType(ApiResultObject result) { Class sourceType = result.getSourceType(); return DateConverter.dateAsLongClass(sourceType); } private String toTypeName(ApiResultObject result) { ApiDataType dataType = result.getDataType(); Class sourceType = getSourceType(result); ; String typeName = sourceType.getSimpleName(); if (dataType.isArrayType()) { typeName = typeName + "[]"; } return typeName; } public static final class FieldInfo { private String type; private String name; private String comment; private String getMethod; private String setMethod; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getGetMethod() { return getMethod; } public void setGetMethod(String getMethod) { this.getMethod = getMethod; } public String getSetMethod() { return setMethod; } public void setSetMethod(String setMethod) { this.setMethod = setMethod; } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/MemoryCodeOutput.java ================================================ package com.terran4j.commons.api2doc.codewriter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class MemoryCodeOutput implements CodeOutput { private final Map codes = new ConcurrentHashMap<>(); @Override public void writeCodeFile(String fileName, String fileContent) { codes.put(fileName, fileContent); } @Override public void setPercent(int percent) { } @Override public void log(String log, String... args) { } public String getCode(String fileName) { return codes.get(fileName); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/RetrofitCodeWriter.java ================================================ package com.terran4j.commons.api2doc.codewriter; import com.terran4j.commons.api2doc.domain.*; import com.terran4j.commons.api2doc.impl.ClasspathFreeMarker; import com.terran4j.commons.util.Classes; import com.terran4j.commons.util.value.KeyedList; import freemarker.template.Template; import freemarker.template.TemplateException; import org.apache.commons.collections4.map.HashedMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import javax.annotation.PostConstruct; import java.io.IOException; import java.util.*; @Service public class RetrofitCodeWriter { private static final Logger log = LoggerFactory.getLogger(RetrofitCodeWriter.class); @Autowired private JavaBeanCodeWriter javaBeanCodeWriter; @Autowired private EnumCodeWriter enumCodeWriter; @Autowired private ClasspathFreeMarker classpathFreeMarker; private Template interfaceTemplate = null; @PostConstruct public void init() { try { interfaceTemplate = classpathFreeMarker.getTemplate(getClass(), // "retrofit.java.ftl"); log.info("RetrofitCodeWriter inited done."); } catch (Exception e) { throw new RuntimeException(e); } } public void writeCode(List folders, CodeOutput out, CodeConfig config) { if (folders == null || folders.size() == 0) { return; } if (out == null) { throw new NullPointerException("CodeWriter writer is null."); } Set> enumClasses = new HashSet<>(); KeyedList javaBeans = new KeyedList(); for (ApiFolderObject folder : folders) { Map model = toModel(folder, config, javaBeans, enumClasses); String className = toRetrofitClassName(folder.getId()); String fileName = className + ".java"; try { String fileContent = classpathFreeMarker.build(interfaceTemplate, model); out.writeCodeFile(fileName, fileContent); } catch (IOException | TemplateException e) { throw new RuntimeException(e); } } Set> writtenClasses = new HashSet<>(); List results = javaBeans.getAll(); for (ApiResultObject result : results) { Class clazz = result.getSourceType(); if (writtenClasses.contains(clazz)) { continue; } String className = clazz.getSimpleName(); try { javaBeanCodeWriter.writeCode(result, className, out, config); } catch (Exception e) { throw new RuntimeException(e); } writtenClasses.add(clazz); } for (Class currentClass : enumClasses) { String className = currentClass.getSimpleName(); try { enumCodeWriter.writeCode(currentClass, className, out, config); } catch (Exception e) { throw new RuntimeException(e); } } } private String toRetrofitClassName(String id) { String name = id.substring(0, 1).toUpperCase(Locale.ENGLISH) + id.substring(1); if (name.endsWith("Controller")) { name = name.replaceAll("Controller", "Retrofit"); } else { name += "Retrofit"; } return name; } private Map toModel(ApiFolderObject folder, CodeConfig config, // KeyedList javaBeans, Set> enumClasses) { Map model = new HashedMap<>(); if (config == null) { config = new CodeConfig(); } model.put("config", config); String className = toRetrofitClassName(folder.getId()); model.put("class", className); String comment = folder.getComment().javadoc(0); if (StringUtils.hasText(comment)) { model.put("comment", comment); } Set imports = new HashSet<>(); model.put("imports", imports); List methods = new ArrayList<>(); List docs = folder.getDocs(); if (docs != null) { for (ApiDocObject doc : docs) { MethodInfo method = toMethodInfo(doc, config, imports); methods.add(method); List results = doc.getResults(); if (results == null || results.size() == 0) { continue; } for (ApiResultObject result : results) { String groupId = result.getGroupId(); if (StringUtils.hasText(groupId) && !javaBeans.containsKey(groupId)) { javaBeans.add(groupId, result); } Class clazz = result.getSourceType(); if (clazz != null && clazz.isEnum()) { enumClasses.add(clazz); } List children = result.getChildren(); if (children != null) { for (ApiResultObject child : children) { clazz = child.getSourceType(); if (clazz != null && clazz.isEnum()) { enumClasses.add(clazz); } } } } } } model.put("methods", methods); return model; } private MethodInfo toMethodInfo(ApiDocObject doc, CodeConfig config, Set imports) { MethodInfo method = new MethodInfo(); method.setName(doc.getId()); String comment = doc.getComment().javadoc(1); method.setComment(comment); List annos = new ArrayList<>(); RequestMethod type = doc.getMethods()[0]; if (type == RequestMethod.POST) { annos.add("@FormUrlEncoded"); } String path = doc.getPaths()[0]; if (!path.endsWith("/")) { path = path + "/"; } String anno = "@" + type.name() + "(\"" + path + "\")"; annos.add(anno); method.setAnnos(annos); List srcParams = new ArrayList<>(); List extraPrams = config.getExtraPrams(doc); if (extraPrams != null) { srcParams.addAll(extraPrams); } if (doc.getParams() != null) { srcParams.addAll(doc.getParams()); } List params = new ArrayList<>(); for (int i = 0; i < srcParams.size(); i++) { ApiParamObject srcParam = srcParams.get(i); ParamInfo param = toParam(srcParam, doc, imports); if (i < srcParams.size() - 1) { param.setExpression(param.getExpression() + ", "); } params.add(param); } method.setParams(params); // 确定返回类型的描述。 String returnClass = null; List results = doc.getResults(); if (results != null && results.size() > 0) { ApiResultObject result = results.get(0); ApiDataType dataType = result.getDataType(); if (dataType != null) { if (dataType == ApiDataType.ARRAY) { returnClass = "List<" + result.getSourceType().getSimpleName() + ">"; } else { returnClass = result.getSourceType().getSimpleName(); } } } if (returnClass == null) { Class returnType = doc.getSourceMethod().getReturnType(); if (returnType != null && returnType != void.class) { returnClass = returnType.getSimpleName(); } } method.setReturnClass(returnClass); return method; } private ParamInfo toParam(ApiParamObject srcParam, ApiDocObject doc, Set imports) { ParamInfo param = new ParamInfo(); String id = srcParam.getId(); param.setId(id); String comment = srcParam.getComment().javadoc(1); param.setComment(comment); StringBuffer expression = new StringBuffer(); RequestMethod requestMethod = doc.getMethods()[0]; ApiParamLocation location = srcParam.getLocation(); String annoName = toParamAnnoName(location, requestMethod); expression.append("@").append(annoName) // .append("(\"").append(id).append("\")"); Class paramClass = Classes.toWrapType(srcParam.getSourceType()); CodeUtils.addImport(paramClass, imports); expression.append(" ").append(paramClass.getSimpleName()); expression.append(" ").append(id); param.setExpression(expression.toString()); return param; } private String toParamAnnoName(ApiParamLocation location, RequestMethod requestMethod) { if (location == ApiParamLocation.RequestHeader) { return "Header"; } else if (location == ApiParamLocation.PathVariable) { return "Path"; } else if (location == ApiParamLocation.RequestParam) { if (requestMethod == RequestMethod.POST) { return "Field"; } else { return "Query"; } } else if (location == ApiParamLocation.CookieValue) { return "Header"; } else if (location == ApiParamLocation.RequestPart) { return "Part"; } else { throw new RuntimeException("ApiParamLocation" + " location unsupported: " + location); } } public static final class ParamInfo { private String id; private String comment; private String expression; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getExpression() { return expression; } public void setExpression(String expression) { this.expression = expression; } } public static final class MethodInfo { private List params; private List annos; private String comment; private String name; private String returnClass; public String getReturnClass() { return returnClass; } public void setReturnClass(String returnClass) { this.returnClass = returnClass; } public List getParams() { return params; } public void setParams(List params) { this.params = params; } public List getAnnos() { return annos; } public void setAnnos(List annos) { this.annos = annos; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getName() { return name; } public void setName(String name) { this.name = name; } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/config/Api2DocConfiguration.java ================================================ package com.terran4j.commons.api2doc.config; import com.terran4j.commons.api2doc.codewriter.RetrofitCodeWriter; import com.terran4j.commons.api2doc.controller.Api2DocController; import com.terran4j.commons.api2doc.impl.Api2DocCollector; import com.terran4j.commons.api2doc.meta.ApiMetaService; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * 可以通过配置 terran4j.api2doc.enabled 来 * 启用或禁用文档服务。 */ @ConditionalOnExpression("${api2doc.enabled:true}") @ComponentScan(basePackageClasses = { Api2DocController.class, Api2DocCollector.class, RetrofitCodeWriter.class, ApiMetaService.class }) @Configuration public class Api2DocConfiguration { } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/config/EnableApi2Doc.java ================================================ package com.terran4j.commons.api2doc.config; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Import; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(Api2DocConfiguration.class) public @interface EnableApi2Doc { } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/Api2DocController.java ================================================ package com.terran4j.commons.api2doc.controller; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.impl.Api2DocProperties; import com.terran4j.commons.api2doc.impl.Api2DocService; import com.terran4j.commons.api2doc.impl.DocMenuBuilder; import com.terran4j.commons.api2doc.impl.DocPageBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; import java.util.Map; @Controller @RequestMapping(value = "/api2doc") public class Api2DocController { private static final Logger log = LoggerFactory.getLogger(Api2DocController.class); @Autowired private DocMenuBuilder docMenuBuilder; @Autowired private DocPageBuilder docPageBuilder; @Autowired private Api2DocService apiDocService; @Autowired private Api2DocProperties api2DocProperties; /** * http://localhost:8080/api2doc/home.html * 整个文档页面,包含顶部标题栏、左侧菜单栏、右侧用 iframe 嵌入的内容区。 */ @RequestMapping(value = "/home.html", method = RequestMethod.GET) public String home(@RequestParam(value = "p", required = false) String p, Map model) throws Exception { String title = api2DocProperties.getApi2docTitle(); if (StringUtils.isEmpty(title)) { String serviceName = api2DocProperties.getServiceName(); if (StringUtils.hasText(serviceName)) { title = serviceName.trim() + "——接口文档"; } } if (StringUtils.isEmpty(title)) { title = "Api2Doc 接口文档"; } model.put("title", title); String icon = api2DocProperties.getApi2docIcon(); if (StringUtils.hasText(icon)) { model.put("icon", icon); } List menus = docMenuBuilder.getMenuGroups(); model.put("menus", menus); // 当前要显示的内容。 String docPath = getDocPath(p); model.put("docPath", docPath); model.put("v", apiDocService.getComponentVersion()); p = p == null ? "" : p; model.put("p", p); if (log.isInfoEnabled()) { log.info("request home.html, model:\n{}", model); } return "api2doc/home"; } private String getDocPath(String p) { String docPath = null; if (StringUtils.hasText(p)) { String[] strs = p.split("-"); if (strs.length >= 3) { String docType = strs[0]; String docGroup = strs[1]; String docId = strs[2]; docPath = String.format("/api2doc/%s/%s/%s.html", docType, docGroup, docId); } } if (docPath == null) { docPath = "/api2doc/welcome.html"; } return apiDocService.addAppDocVersion(docPath); } /** * http://localhost:8080/api2doc/welcome.html * 文档首页内容。 */ @RequestMapping(value = "/welcome.html", method = RequestMethod.GET) public String welcome(Map model) throws Exception { String md = docPageBuilder.loadMdFromResource("welcome.md"); return md2HtmlPage(md, null, model); } /** * http://localhost:8080/api2doc/overview.html */ @RequestMapping(value = "/md/{folderId}/{docId}.html", method = RequestMethod.GET) public String md(@PathVariable("folderId") String folderId, @PathVariable("docId") String docId, Map model) throws Exception { String md = docPageBuilder.loadMdFromResource(folderId, docId); return md2HtmlPage(md, null, model); } @RequestMapping(value = "/api/{fid}/{id}.html", method = RequestMethod.GET) public String api2doc( @PathVariable("fid") String folderId, @PathVariable("id") String id, Map model) throws Exception { ApiDocObject doc = apiDocService.getDocObject(folderId, id); String md = docPageBuilder.doc2Md(doc); String title = doc.getName(); return md2HtmlPage(md, title, model); } @RequestMapping(value = "/test/{fid}/{id}.html", method = RequestMethod.GET) public String api2test( @PathVariable("fid") String folderId, @PathVariable("id") String id, Map model) throws Exception { ApiDocObject doc = apiDocService.getDocObject(folderId, id); model.put("doc", doc); model.put("v", apiDocService.getComponentVersion()); // String md = docPageBuilder.doc2Md(doc); // String title = doc.getName(); return "api2doc/test"; } public String md2HtmlPage(String md, String title, Map model) throws Exception { if (title != null) { model.put("title", title); } String html = docPageBuilder.md2Html(md); model.put("content", html); model.put("v", apiDocService.getComponentVersion()); return "api2doc/doc"; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiEntry.java ================================================ package com.terran4j.commons.api2doc.controller; public class ApiEntry { private String key; private String value; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ApiEntry apiEntry = (ApiEntry) o; if (key != null ? !key.equals(apiEntry.key) : apiEntry.key != null) return false; return value != null ? value.equals(apiEntry.value) : apiEntry.value == null; } @Override public int hashCode() { int result = key != null ? key.hashCode() : 0; result = 31 * result + (value != null ? value.hashCode() : 0); return result; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiInfo.java ================================================ package com.terran4j.commons.api2doc.controller; import java.util.Arrays; import java.util.List; public class ApiInfo { String[] methods; String defaultMethod; String url; List params; List headers; public String[] getMethods() { return methods; } public void setMethods(String[] methods) { this.methods = methods; } public String getDefaultMethod() { return defaultMethod; } public void setDefaultMethod(String defaultMethod) { this.defaultMethod = defaultMethod; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public List getParams() { return params; } public void setParams(List params) { this.params = params; } public List getHeaders() { return headers; } public void setHeaders(List headers) { this.headers = headers; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ApiInfo apiInfo = (ApiInfo) o; // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(methods, apiInfo.methods)) return false; if (defaultMethod != null ? !defaultMethod.equals(apiInfo.defaultMethod) : apiInfo.defaultMethod != null) return false; if (url != null ? !url.equals(apiInfo.url) : apiInfo.url != null) return false; if (params != null ? !params.equals(apiInfo.params) : apiInfo.params != null) return false; return headers != null ? headers.equals(apiInfo.headers) : apiInfo.headers == null; } @Override public int hashCode() { int result = Arrays.hashCode(methods); result = 31 * result + (defaultMethod != null ? defaultMethod.hashCode() : 0); result = 31 * result + (url != null ? url.hashCode() : 0); result = 31 * result + (params != null ? params.hashCode() : 0); result = 31 * result + (headers != null ? headers.hashCode() : 0); return result; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiMetaController.java ================================================ package com.terran4j.commons.api2doc.controller; import com.terran4j.commons.api2doc.meta.ApiMetaService; import com.terran4j.commons.api2doc.meta.ClassMeta; import com.terran4j.commons.restpack.RestPackController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.List; @RestPackController @RequestMapping(value = "/api2doc/meta") public class ApiMetaController { @Autowired private ApiMetaService apiMetaService; @RequestMapping(value = "/classes", method = RequestMethod.GET) public List getClassMetaList() throws Exception { return apiMetaService.toClassMetaList(); } @RequestMapping(value = "/apiInfo/{fid}/{id}", method = RequestMethod.GET) public ApiInfo getApiInfo(@PathVariable String fid, @PathVariable String id) throws Exception { return apiMetaService.toApiInfo(fid, id); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/MenuData.java ================================================ package com.terran4j.commons.api2doc.controller; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.util.Strings; import java.util.List; public class MenuData implements Comparable{ private boolean folder; private int order = Api2Doc.DEFAULT_ORDER; private String id; private String index; private String name; private String url; private List children; public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } public final boolean isFolder() { return folder; } public final void setFolder(boolean folder) { this.folder = folder; } public final String getId() { return id; } public final void setId(String id) { this.id = id; } public final String getIndex() { return index; } public final void setIndex(String index) { this.index = index; } public final String getName() { return name; } public final void setName(String name) { this.name = name; } public final List getChildren() { return children; } public final void setChildren(List children) { this.children = children; } public final String getUrl() { return url; } public final void setUrl(String url) { this.url = url; } public final String toString() { return Strings.toString(this); } @Override public int compareTo( MenuData other) { MenuData o1 = this; MenuData o2 = other; if (o1.getOrder() < o2.getOrder()) { return -1; } if (o1.getOrder() > o2.getOrder()) { return 1; } return 0; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDataType.java ================================================ package com.terran4j.commons.api2doc.domain; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.module.jsonSchema.JsonSchema; import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; import com.terran4j.commons.util.Jsons; /** * api的基本数据类型。 * * @author jiangwei */ public enum ApiDataType { BOOLEAN("boolean") { @Override public String getDefault() { return "false"; } @Override public Object parseValue(String text) { return Boolean.parseBoolean(text); } @Override public boolean isSimpleType() { return true; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return false; } }, INT("int") { @Override public String getDefault() { return "0"; } @Override public Object parseValue(String text) { return Integer.parseInt(text); } @Override public boolean isSimpleType() { return true; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return false; } }, LONG("long") { @Override public String getDefault() { return "0"; } @Override public Object parseValue(String text) { return Long.parseLong(text); } @Override public boolean isSimpleType() { return true; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return false; } }, NUMBER("number") { @Override public String getDefault() { return "0.1"; } @Override public Object parseValue(String text) { return Double.parseDouble(text); } @Override public boolean isSimpleType() { return true; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return false; } }, STRING("string") { @Override public String getDefault() { return "my-string"; } @Override public Object parseValue(String text) { return text; } @Override public boolean isSimpleType() { return true; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return false; } }, ARRAY("array") { @Override public String getDefault() { return "[]"; } @Override public Object parseValue(String text) { throw new UnsupportedOperationException("array can't parse from text: " + text); } @Override public boolean isSimpleType() { return false; } @Override public boolean isArrayType() { return true; } @Override public boolean isObjectType() { return false; } }, OBJECT("object") { @Override public String getDefault() { return "{}"; } @Override public Object parseValue(String text) { throw new UnsupportedOperationException("object can't parse from text: " + text); } @Override public boolean isSimpleType() { return false; } @Override public boolean isArrayType() { return false; } @Override public boolean isObjectType() { return true; } }; private final String name; ApiDataType(String name) { this.name = name; } public String getName() { return name; } public abstract String getDefault(); public abstract Object parseValue(String text); public abstract boolean isSimpleType(); public abstract boolean isArrayType(); public abstract boolean isObjectType(); private static JsonSchemaGenerator schemaGen = null; public static final JsonSchemaGenerator getJsonSchemaGenerator() { if (schemaGen != null) { return schemaGen; } synchronized (ApiDataType.class) { if (schemaGen != null) { return schemaGen; } try { schemaGen = new JsonSchemaGenerator(Jsons.getObjectMapper()); return schemaGen; } catch (Exception e) { throw new RuntimeException(e); } } } public static ApiDataType toDataType(Class clazz) { // 日期类型,按 long 返回。 ApiDataType dataType = DateConverter.dateAsLongType(clazz); if (dataType != null) { return dataType; } try { JsonSchema schema = getJsonSchemaGenerator().generateSchema(clazz); return toDataType(schema); } catch (JsonMappingException e) { String msg = "generate schema by class failed, class = " + clazz.getName(); throw new RuntimeException(msg, e); } } public static ApiDataType toDataType(JsonSchema schema) { if (schema == null) { return null; } if (schema.isBooleanSchema()) { return ApiDataType.BOOLEAN; } if (schema.isIntegerSchema()) { return ApiDataType.INT; } if (schema.isStringSchema()) { return ApiDataType.STRING; } if (schema.isNumberSchema()) { return ApiDataType.NUMBER; } if (schema.isObjectSchema()) { return ApiDataType.OBJECT; } if (schema.isArraySchema()) { return ApiDataType.ARRAY; } return null; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDocObject.java ================================================ package com.terran4j.commons.api2doc.domain; import com.terran4j.commons.api2doc.impl.Api2DocObjectFactory; import com.terran4j.commons.util.value.KeyedList; import org.springframework.web.bind.annotation.RequestMethod; import java.lang.reflect.Method; import java.util.List; public class ApiDocObject extends ApiObject { private ApiFolderObject folder; private String[] paths; private Method sourceMethod; private RequestMethod[] methods; private String returnTypeDesc; private List results; private ApiResultObject resultType; private final KeyedList params = new KeyedList<>(); private final KeyedList errors = new KeyedList<>(); public ApiResultObject getResultType() { return resultType; } public void setResultType(ApiResultObject resultType) { this.resultType = resultType; } public Method getSourceMethod() { return sourceMethod; } public void setSourceMethod(Method sourceMethod) { this.sourceMethod = sourceMethod; } public String[] getPaths() { return paths; } public void setPaths(String[] paths) { this.paths = paths; } public RequestMethod[] getMethods() { return methods; } public void setMethods(RequestMethod[] methods) { this.methods = methods; } public final List getParams() { return params.getAll(); } public final ApiParamObject getParam(String id) { return params.get(id); } public final void addParam(ApiParamObject param) { this.params.add(param.getId(), param); } public List getResults() { return results; } public void setResults(List results) { this.results = results; } public List getErrors() { return errors.getAll(); } public void addError(ApiErrorObject error) { this.errors.add(error.getId(), error); } public ApiFolderObject getFolder() { return folder; } public void setFolder(ApiFolderObject folder) { this.folder = folder; } public String getReturnTypeDesc() { return returnTypeDesc; } public void setReturnTypeDesc(String returnTypeDesc) { this.returnTypeDesc = returnTypeDesc; } public final Object toMockResult() { List results = getResults(); if (results != null && results.size() > 0) { ApiResultObject result = results.get(0); return Api2DocObjectFactory.createObject(result.getDataType(), result.getSourceType(), result.getSample().getValue()); } if (resultType != null) { return Api2DocObjectFactory.createObject(resultType.getDataType(), resultType.getSourceType(), resultType.getSample().getValue()); } return null; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDocUtils.java ================================================ package com.terran4j.commons.api2doc.domain; import org.springframework.util.StringUtils; import com.terran4j.commons.api2doc.annotations.Api2Doc; public class ApiDocUtils { public static final String getId(Class clazz) { if (clazz == null) { throw new NullPointerException(); } Api2Doc api2doc = clazz.getAnnotation(Api2Doc.class); if (api2doc != null) { String id = api2doc.id(); if (StringUtils.hasText(id)) { return id; } String value = api2doc.value(); if (StringUtils.hasText(value)) { return value; } } return clazz.getName(); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiErrorObject.java ================================================ package com.terran4j.commons.api2doc.domain; public class ApiErrorObject extends ApiObject { } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiFolderObject.java ================================================ package com.terran4j.commons.api2doc.domain; import com.terran4j.commons.util.value.KeyedList; import org.springframework.util.StringUtils; import java.util.List; import java.util.Map; public class ApiFolderObject extends ApiObject { private boolean restPack = false; private Map mds; private Class sourceClass; private final KeyedList docs = new KeyedList<>(); public boolean isRestPack() { return restPack; } public void setRestPack(boolean restPack) { this.restPack = restPack; } public Map getMds() { return mds; } public void setMds(Map mds) { this.mds = mds; } public Class getSourceClass() { return sourceClass; } public void setSourceClass(Class sourceClass) { this.sourceClass = sourceClass; } public final List getDocs() { return docs.getAll(); } public final ApiDocObject getDoc(String id) { return docs.get(id); } public final void addDocs(List docList) { if (docList == null) { return; } for (ApiDocObject doc : docList) { addDoc(doc); } } public final void addDoc(ApiDocObject doc) { if (doc == null) { throw new NullPointerException(); } String id = doc.getId(); if (StringUtils.isEmpty(id)) { throw new NullPointerException("doc id is empty."); } this.docs.add(id, doc); } public static final String name2Id(String name) { int hash = name.hashCode(); String id; if (hash < 0) { id = "n" + Math.abs(hash); } else { id = String.valueOf(hash); } return id; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiObject.java ================================================ package com.terran4j.commons.api2doc.domain; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.api2doc.impl.FlexibleString; import com.terran4j.commons.util.Strings; public class ApiObject implements Comparable{ private String id; private String name; private final FlexibleString comment = new FlexibleString(); private final FlexibleString sample = new FlexibleString(); private int order = Api2Doc.DEFAULT_ORDER; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public final int getOrder() { return order; } public final void setOrder(int order) { this.order = order; } public void insertComment(String comment) { this.comment.insertLine(comment); } public FlexibleString getComment() { return comment; } public void setComment(String comment) { this.comment.setValue(comment); } public FlexibleString getSample() { return sample; } public void setSample(String sample) { this.sample.setValue(sample); } @Override public final String toString() { return Strings.toString(this); } @Override public int compareTo(ApiObject other) { ApiObject o1 = this; ApiObject o2 = other; // 优先按用户指定排序。 if (o1.getOrder() < o2.getOrder()) { return -1; } if (o1.getOrder() > o2.getOrder()) { return 1; } // 其次按 id 字符串排序。 return o1.getId().compareTo(o2.getId()); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiParamLocation.java ================================================ package com.terran4j.commons.api2doc.domain; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.lang.reflect.AnnotatedElement; /** * API 参数在 HTTP 协议中的位置。 * * @author jiangwei */ public enum ApiParamLocation { RequestHeader { @Override boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) { RequestHeader requestHeader = element.getAnnotation(RequestHeader.class); if (requestHeader == null) { return false; } String name = null; if (StringUtils.hasText(requestHeader.value())) { name = requestHeader.value(); } if (StringUtils.hasText(requestHeader.name())) { name = requestHeader.name(); } apiParamObject.setName(name); boolean required = requestHeader.required(); apiParamObject.setRequired(required); String paramSample = requestHeader.defaultValue(); if (StringUtils.hasText(paramSample)) { if (ValueConstants.DEFAULT_NONE.equals(paramSample)) { paramSample = ""; } apiParamObject.setSample(paramSample); } return true; } }, RequestParam { @Override boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) { RequestParam requestParam = element.getAnnotation(RequestParam.class); if (requestParam == null) { return false; } String name = null; if (StringUtils.hasText(requestParam.value())) { name = requestParam.value(); } if (StringUtils.hasText(requestParam.name())) { name = requestParam.name(); } apiParamObject.setName(name); boolean required = requestParam.required(); apiParamObject.setRequired(required); String paramSample = requestParam.defaultValue(); if (StringUtils.hasText(paramSample)) { if (ValueConstants.DEFAULT_NONE.equals(paramSample)) { paramSample = ""; } apiParamObject.setSample(paramSample); } return true; } }, RequestPart { @Override boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) { RequestPart requestPart = element.getAnnotation(RequestPart.class); if (requestPart == null) { return false; } String name = null; if (StringUtils.hasText(requestPart.value())) { name = requestPart.value(); } if (StringUtils.hasText(requestPart.name())) { name = requestPart.name(); } apiParamObject.setName(name); boolean required = requestPart.required(); apiParamObject.setRequired(required); return true; } }, CookieValue { @Override boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) { CookieValue cookieValue = element.getAnnotation(CookieValue.class); if (cookieValue == null) { return false; } String name = null; if (StringUtils.hasText(cookieValue.value())) { name = cookieValue.value(); } if (StringUtils.hasText(cookieValue.name())) { name = cookieValue.name(); } apiParamObject.setName(name); boolean required = cookieValue.required(); apiParamObject.setRequired(required); String paramSample = cookieValue.defaultValue(); if (StringUtils.hasText(paramSample)) { if (ValueConstants.DEFAULT_NONE.equals(paramSample)) { paramSample = ""; } apiParamObject.setSample(paramSample); } return true; } }, PathVariable { @Override boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) { PathVariable pathVariable = element.getAnnotation(PathVariable.class); if (pathVariable == null) { return false; } String name = null; if (StringUtils.hasText(pathVariable.value())) { name = pathVariable.value(); } if (StringUtils.hasText(pathVariable.name())) { name = pathVariable.name(); } apiParamObject.setName(name); boolean required = pathVariable.required(); apiParamObject.setRequired(required); return true; } }; public static final ApiParamLocation[] API_PARAM_LOCATIONS = new ApiParamLocation[]{ RequestParam, PathVariable, RequestHeader, CookieValue, RequestPart }; abstract boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement param); public static final void collects(ApiParamObject apiParamObject, AnnotatedElement element) { ApiParamLocation currentLocation = ApiParamLocation.RequestParam; for (ApiParamLocation location : API_PARAM_LOCATIONS) { if (location.doCollect(apiParamObject, element)) { currentLocation = location; break; } } apiParamObject.setLocation(currentLocation); String sample = apiParamObject.getSample().getValue(); if (ValueConstants.DEFAULT_NONE.equals(sample)) { apiParamObject.setSample(""); } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiParamObject.java ================================================ package com.terran4j.commons.api2doc.domain; public class ApiParamObject extends ApiObject { private boolean required; private ApiDataType dataType; private ApiParamLocation location; private Class sourceType; public ApiParamObject() { super(); this.setComment(""); } public Class getSourceType() { return sourceType; } public void setSourceType(Class sourceType) { this.sourceType = sourceType; } public String getTypeName() { if (dataType == null) { return ""; } if (dataType.isArrayType()) { return dataType.name().toLowerCase() + "[]"; } return dataType.name().toLowerCase(); } public ApiDataType getDataType() { return dataType; } public void setDataType(ApiDataType type) { this.dataType = type; } public boolean isRequired() { return required; } public void setRequired(boolean required) { this.required = required; } public final String getRequiredName() { return required ? "是" : "否"; } public ApiParamLocation getLocation() { return location; } public void setLocation(ApiParamLocation location) { this.location = location; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiResultObject.java ================================================ package com.terran4j.commons.api2doc.domain; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.impl.Api2DocUtils; import com.terran4j.commons.api2doc.impl.ApiCommentUtils; import com.terran4j.commons.util.Classes; import com.terran4j.commons.util.value.KeyedList; import org.apache.commons.beanutils.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * 记录所有的结果字段,它是一个复合型 */ public class ApiResultObject extends ApiObject { private static final Logger log = LoggerFactory.getLogger(ApiResultObject.class); private ApiDataType dataType; /** * 如果类型是数组类型,此类是里面元素的类型 * 否则是这个类型本身。 */ private Class sourceType; private String typeName = ""; private String refGroupId = null; private String groupId = null; private String groupName = null; private final List children = new ArrayList<>(); public Class getSourceType() { return sourceType; } public void setSourceType(Class sourceType) { this.sourceType = sourceType; } public String getRefGroupId() { return refGroupId; } public void setRefGroupId(String refGroupId) { this.refGroupId = refGroupId; } public final ApiDataType getDataType() { return dataType; } public final void setDataType(ApiDataType dataType) { this.dataType = dataType; } public String getTypeName() { return typeName; } public void setTypeName(String typeName) { this.typeName = typeName; } public final List getChildren() { return children; } public final ApiResultObject getChild(String fieldName) { if (children == null || StringUtils.isEmpty(fieldName)) { return null; } for (ApiResultObject child : children) { if (fieldName.equals(child.getId())) { return child; } } return null; } public final void addChild(ApiResultObject child) { this.children.add(child); } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } @SuppressWarnings({"unchecked", "rawtypes"}) public static final String getEnumComment(Class clazz) { if (clazz == null) { return null; } if (!clazz.isEnum()) { return null; } StringBuffer sb = new StringBuffer("\n可选值为:"); Class> enumClass = (Class>) clazz; Enum[] enums = enumClass.getEnumConstants(); for (Enum e : enums) { String name = e.name(); Field field = null; try { field = enumClass.getDeclaredField(name); } catch (NoSuchFieldException | SecurityException e1) { log.error("Can't get field \"" + name + "\" from Enum: " + clazz.getName(), e1); continue; } ApiComment comment = field.getAnnotation(ApiComment.class); String value = ApiCommentUtils.getComment( comment, null, field.getName()); if (value == null) { value = ""; } if (sb.length() > 0) { sb.append("\n"); } sb.append(name).append(": ").append(value).append("; "); } return sb.toString(); } private static final String getTypeName(Class clazz, ApiDataType dataType) { if (clazz.isEnum()) { return ApiDataType.STRING.name().toLowerCase() + "(枚举值)"; } else if (dataType != null && dataType.isSimpleType()) { return dataType.name().toLowerCase(); } else { return clazz.getSimpleName(); } } /** * 找到一个方法返回类型中字段,收集它的 Api2Doc 信息。 * * @param method * @param totalResults * @return */ public static final ApiResultObject parseResultType( Method method, KeyedList totalResults) { if (method == null) { return null; } if (totalResults == null) { totalResults = new KeyedList<>(); } final Class clazz = method.getReturnType(); final ApiDataType dataType = ApiDataType.toDataType(clazz); if (dataType == null) { return null; } String typeName = getTypeName(clazz, dataType); // 基本类型,直接处理。 if (dataType.isSimpleType()) { return createSimple(clazz, clazz, dataType, typeName); } // 子类型。 Class elementType = null; // 数组类型,找到它的元素的具体类型,然后处理具体类型。 if (dataType.isArrayType()) { elementType = Api2DocUtils.getArrayElementClass(method); if (elementType == null) { log.warn("Can't find element class by method: {}", method); return null; } ApiDataType elementDataType = ApiDataType.toDataType(elementType); typeName = getTypeName(elementType, elementDataType) + "[]"; // 数组类型,但元素是基本类型的,也直接处理。 if (elementDataType != null && elementDataType.isSimpleType()) { return createSimple(elementType, clazz, dataType, typeName); } } // 复杂类型的情况。 ApiResultObject result = new ApiResultObject(); result.setDataType(dataType); result.setSourceType(clazz); result.setTypeName(typeName); result.setId(""); if (dataType.isObjectType()) { elementType = method.getReturnType(); } // 没有子类型,直接返回。 // TODO: 暂时不解析 Map 内部的类型。 if (elementType == null || Map.class.equals(elementType)) { return result; } result.setSourceType(elementType); // 没有子类型,直接返回。 PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(elementType); if (props == null || props.length == 0) { return result; } // 根据类型生成字段集的 id 和 name 。 String groupId = getGroupId(elementType); result.setGroupId(groupId); String groupName = elementType.getSimpleName(); result.setGroupName(groupName); // 加入到结果字段集中。 if (totalResults.containsKey(groupId)) { return result; } else { totalResults.add(groupId, result); } // 有子类型,补充子类型信息。 for (PropertyDescriptor prop : props) { if (Api2DocUtils.isFilter(prop, elementType)) { continue; } String fieldName = prop.getName(); Method subMethod = prop.getReadMethod(); // 处理子类型。 ApiResultObject childPropResult; try { childPropResult = parseResultType(subMethod, totalResults); } catch (Exception e) { String msg = String.format("解析类[ %s ]的属性[ %s ]出错: %s", elementType.getName(), fieldName, e.getMessage()); throw new RuntimeException(msg); } // 补充子类型信息。 if (childPropResult != null) { // 补充到当前节点中。 result.addChild(childPropResult); String id = prop.getName(); childPropResult.setId(id); childPropResult.setName(id); Class childPropClass = subMethod.getReturnType(); ApiDataType childPropDataType = ApiDataType.toDataType(childPropClass); childPropResult.setDataType(childPropDataType); Api2Doc childApi2Doc; ApiComment childApiComment; String childName; Field field = Classes.getField(id, elementType); if (field != null) { childApiComment = field.getAnnotation(ApiComment.class); childApi2Doc = field.getAnnotation(Api2Doc.class); childName = field.getName(); } else { childApiComment = subMethod.getAnnotation(ApiComment.class); childApi2Doc = subMethod.getAnnotation(Api2Doc.class); childName = subMethod.getName(); } ApiComment elementApiComment = elementType .getAnnotation(ApiComment.class); Class defaultSeeClass = ApiCommentUtils .getDefaultSeeClass(elementApiComment, null); String comment = ApiCommentUtils.getComment( childApiComment, defaultSeeClass, childName); if (comment == null) { comment = ""; } childPropResult.insertComment(comment); String sample = ApiCommentUtils.getSample( childApiComment, defaultSeeClass, childName); if (sample == null) { sample = ""; } childPropResult.setSample(sample); if (childApi2Doc != null) { childPropResult.setOrder(childApi2Doc.order()); } // 记录所引用的类型。 Class childSubType = null; if (childPropDataType != null) { if (childPropDataType.isArrayType()) { childSubType = Api2DocUtils.getArrayElementClass(subMethod); } else if (childPropDataType.isObjectType()) { childSubType = subMethod.getReturnType(); } } if (childSubType != null) { String refGroupId = getGroupId(childSubType); childPropResult.setRefGroupId(refGroupId); } } } Collections.sort(result.getChildren()); return result; } public static final String getGroupId(Class clazz) { if (clazz == null) { throw new NullPointerException(); } String groupId = ApiDocUtils.getId(clazz); return groupId; } private static ApiResultObject createSimple(Class sourceType, Class clazz, ApiDataType dataType, String typeName) { ApiResultObject result = new ApiResultObject(); result.setSourceType(sourceType); result.setDataType(dataType); result.setTypeName(typeName); result.insertComment(getEnumComment(clazz)); result.setId(""); return result; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/DateConverter.java ================================================ package com.terran4j.commons.api2doc.domain; import java.util.Date; public class DateConverter { public static boolean isDateType(Class clazz) { return clazz == Date.class || clazz == java.sql.Date.class; } public static Object dateAsLongValue(Class clazz) { if (isDateType(clazz)) { // 这里不能用 Long 类型,因为赋值会失败。 return new Date(); } return null; } public static ApiDataType dateAsLongType(Class clazz) { if (isDateType(clazz)) { return ApiDataType.LONG; } return null; } public static Class dateAsLongClass(Class clazz) { if (isDateType(clazz)) { return Long.class; } return clazz; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocCollector.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.annotations.Api2Doc; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.annotations.ApiError; import com.terran4j.commons.api2doc.annotations.ApiErrors; import com.terran4j.commons.api2doc.domain.*; import com.terran4j.commons.restpack.RestPackController; import com.terran4j.commons.util.Classes; import com.terran4j.commons.util.error.BusinessException; import com.terran4j.commons.util.value.KeyedList; import org.apache.commons.beanutils.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.Resource; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; @Service public class Api2DocCollector implements BeanPostProcessor { private static final Logger log = LoggerFactory.getLogger(Api2DocCollector.class); private final LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); @Autowired private Api2DocService apiDocService; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { ApiFolderObject folder; try { folder = toApiFolder(bean, beanName); } catch (BusinessException e) { throw new BeanDefinitionStoreException( "bean上的文档信息定义出错:" + e.getMessage()); } if (folder == null) { return bean; } String id = folder.getId(); ApiFolderObject existApiFolder = apiDocService.getFolder(id); if (existApiFolder != null) { String msg = "@Api2Doc id值重复: " + id; throw new BeanDefinitionStoreException(msg); } if (folder != null) { apiDocService.addFolder(folder); } return bean; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } /** * 解析 API 组,一组 API 对应一个 Controller 类, 其中每个 method 对应一个 api 。
* 只要有 @ApiDoc 注解,有会生成文档,没有这个注解就不会。 * * @param bean * @param beanName * @return */ public ApiFolderObject toApiFolder(Object bean, String beanName) throws BusinessException { Class clazz = Classes.getTargetClass(bean); Controller controller = AnnotationUtils.findAnnotation(clazz, Controller.class); if (controller == null) { // 不是 Controller 类,不用收集。 return null; } if (log.isInfoEnabled()) { log.info("prepare to get API Info by bean: {}", beanName); } List methods = MappingMethod.getMappingMethods(clazz); // Classes.getMethods(RequestMapping.class, clazz); if (methods == null || methods.size() == 0) { // 整个类中都没有任何 RequestMapping 的方法,不用收集。 // if (log.isInfoEnabled()) { // log.info("No any @RequestMapping / method, no need to get, " + // "beanName = {}", beanName); // } return null; } Api2Doc classApi2Doc = clazz.getAnnotation(Api2Doc.class); if (classApi2Doc != null && classApi2Doc.ignore()) { // 整个类的文档被忽略。 if (log.isInfoEnabled()) { log.info("@Api2Doc ignore = true, no need to get, " + "beanName = {}", beanName); } return null; } List ali2DocMethods = new ArrayList<>(); for (MappingMethod mappingMethod : methods) { Method method = mappingMethod.getMethod(); Api2Doc api2Doc = method.getAnnotation(Api2Doc.class); if (classApi2Doc == null && api2Doc == null) { // 本方法的文档被忽略。 continue; } if (api2Doc != null && api2Doc.ignore()) { // 本方法的文档被忽略。 continue; } ali2DocMethods.add(mappingMethod); } if (classApi2Doc == null && ali2DocMethods.size() == 0) { // 整个类中的方法,都忽略从 API 生成文档,不用收集。 if (log.isInfoEnabled()) { log.info("all method were ignored, no need to get, beanName = {}", beanName); } return null; } ApiFolderObject folder = new ApiFolderObject(); folder.setSourceClass(clazz); String id = beanName; if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.value())) { id = classApi2Doc.value(); } if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.id())) { id = classApi2Doc.id(); } folder.setId(id); checkId(id); String pathPattern = "api2doc/" + id + "/*.md"; try { Resource[] resources = Classes.scanResources(pathPattern); if (resources != null && resources.length > 0) { Map mds = new HashMap<>(); for (Resource resource : resources) { String md = resource.getFilename(); mds.put(ApiFolderObject.name2Id(md), md); } folder.setMds(mds); } } catch (IOException e) { String msg = "scan classpath[" + pathPattern + "] failed: " + e.getMessage(); throw new BeanDefinitionStoreException(msg); } if (classApi2Doc != null) { folder.setOrder(classApi2Doc.order()); } // API 组的名称。 String name = beanName; RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class); if (classMapping != null && StringUtils.hasText(classMapping.name())) { name = classMapping.name(); } if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.name())) { name = classApi2Doc.name(); } folder.setName(name); // 这组 API 是否用了 RestPack RestPackController restPackController = clazz.getAnnotation(RestPackController.class); folder.setRestPack(restPackController != null); // API 组的注释。 ApiComment apiComment = clazz.getAnnotation(ApiComment.class); folder.setComment(ApiCommentUtils.getComment( apiComment, null, null)); // 在类上的 seeClass ,是默认的。 Class defaultSeeClass = ApiCommentUtils.getDefaultSeeClass( apiComment, null); // API 组的路径前缀。 String[] basePaths = getPath(classMapping); // 根据方法生成 API 文档。 List docs = new ArrayList<>(); for (MappingMethod method : ali2DocMethods) { ApiDocObject doc = getApiDoc(method, basePaths, beanName, classApi2Doc, defaultSeeClass); if (doc == null) { continue; } String docId = doc.getId(); ApiDocObject existDoc = folder.getDoc(docId); if (existDoc != null) { String msg = "文档id值重复: " + docId + "\n" + "如果方法上没有用 @Api2Doc(id = \"xxx\") 来指定文档id,则重载方法会出现此问题。\n" + "请在重载的方法上用 @Api2Doc(id = \"xxx\") 来指定一个不同的文档id"; throw new BeanDefinitionStoreException(msg); } doc.setFolder(folder); docs.add(doc); if (log.isInfoEnabled()) { log.info("add doc: {}/{}", folder.getId(), docId); } } Collections.sort(docs); folder.addDocs(docs); return folder; } ApiDocObject getApiDoc( MappingMethod mappingMethod, String[] basePaths, String beanName, Api2Doc classApi2Doc, Class defaultSeeClass) throws BusinessException { Method method = mappingMethod.getMethod(); // 只要有 @ApiDoc 注解(无论是本方法上,还是类上),有会生成文档,没有这个注解就不会。 Api2Doc api2Doc = method.getAnnotation(Api2Doc.class); if (api2Doc == null && classApi2Doc == null) { return null; } ApiDocObject doc = new ApiDocObject(); doc.setSourceMethod(method); // 获取文档的 id,以 @Api2Doc、方法名 为顺序获取。 String id = method.getName(); if (api2Doc != null && StringUtils.hasText(api2Doc.value())) { id = api2Doc.value(); } if (api2Doc != null && StringUtils.hasText(api2Doc.id())) { id = api2Doc.id(); } doc.setId(id); checkId(id); // 获取文档的排序。 if (api2Doc != null) { doc.setOrder(api2Doc.order()); } // 获取文档名称,按 @Api2Doc 、@Mapping、方法名的顺序获取。 String name = method.getName(); String mappingName = mappingMethod.getName(); if (StringUtils.hasText(mappingName)) { name = mappingName; } if (api2Doc != null && StringUtils.hasText(api2Doc.name())) { name = api2Doc.name(); } doc.setName(name); // 获取 API 的注释信息。 ApiComment apiComment = method.getAnnotation(ApiComment.class); defaultSeeClass = ApiCommentUtils.getDefaultSeeClass(apiComment, defaultSeeClass); String docComment = ApiCommentUtils.getComment(apiComment, defaultSeeClass, name); doc.setComment(docComment); String docSample = ApiCommentUtils.getSample(apiComment, defaultSeeClass, name); doc.setSample(docSample); // 获取 API 的访问路径。 String[] paths = mappingMethod.getPath(); paths = combine(basePaths, paths); doc.setPaths(paths); // 获取 HTTP 方法。 doc.setMethods(mappingMethod.getRequestMethod()); // 收集参数信息。 List apiParams = toApiParams(method, defaultSeeClass); if (apiParams != null && apiParams.size() > 0) { for (ApiParamObject apiParam : apiParams) { doc.addParam(apiParam); } } // 收集返回值信息。 KeyedList totalResults = new KeyedList<>(); ApiResultObject resultObject = ApiResultObject.parseResultType(method, totalResults); if (resultObject != null) { resultObject.setComment(docComment); resultObject.setSample(docSample); } doc.setResultType(resultObject); doc.setResults(totalResults.getAll()); // 确定返回类型的描述。 String returnTypeDesc = null; List results = doc.getResults(); if (results != null && results.size() > 0) { ApiResultObject result = results.get(0); ApiDataType dataType = result.getDataType(); if (dataType != null) { if (dataType == ApiDataType.ARRAY) { returnTypeDesc = result.getSourceType().getSimpleName() + "[]"; } else { returnTypeDesc = result.getSourceType().getSimpleName(); } } } if (returnTypeDesc == null) { Class returnType = doc.getSourceMethod().getReturnType(); if (returnType != null && returnType != void.class) { returnTypeDesc = returnType.getSimpleName(); } } doc.setReturnTypeDesc(returnTypeDesc); // 收集错误码信息。 ApiErrors errorCodes = method.getAnnotation(ApiErrors.class); if (errorCodes != null && errorCodes.value() != null && errorCodes.value().length > 0) { for (ApiError errorCode : errorCodes.value()) { ApiErrorObject error = getError(errorCode); if (error == null) { continue; } doc.addError(error); } } else { ApiError errorCode = method.getAnnotation(ApiError.class); ApiErrorObject error = getError(errorCode); if (error != null) { doc.addError(error); } } return doc; } public List toApiParams(Method method, Class defaultSeeClass) { List result = new ArrayList<>(); Set paramIds = new HashSet<>(); Parameter[] params = method.getParameters(); if (params != null && params.length > 0) { String[] paramNames = parameterNameDiscoverer.getParameterNames(method); for (int i = 0; i < params.length; i++) { Parameter param = params[i]; Class paramClass = param.getType(); ApiComment paramClassComment = paramClass.getAnnotation(ApiComment.class); if (paramClassComment != null) { // 从参数的类的属性中获取注释信息。 List paramsFromClass = toApiParams( paramClass, defaultSeeClass); for (ApiParamObject paramFromClass : paramsFromClass) { if (paramIds.contains(paramFromClass.getId())) { continue; } paramIds.add(paramFromClass.getId()); result.add(paramFromClass); } } else { // 从参数本身中获取注释信息。 String paramName; if (paramNames != null) { paramName = paramNames[i]; } else { paramName = param.getName(); } ApiParamObject apiParamObject = toApiParam(param, paramName, param.getType(), defaultSeeClass); if (apiParamObject == null) { continue; } String paramId = apiParamObject.getId(); if (paramIds.contains(paramId)) { String msg = "参数id值重复: " + paramId + ",所在方法: " + method; throw new BeanDefinitionStoreException(msg); } paramIds.add(paramId); result.add(apiParamObject); } } } return result; } /** * 从类的属性中获取注释信息。 * * @param beanClass * @param defaultSeeClass * @return */ public List toApiParams(Class beanClass, Class defaultSeeClass) { List result = new ArrayList<>(); PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(beanClass); if (props == null || props.length == 0) { return result; } for (PropertyDescriptor prop : props) { if (Api2DocUtils.isFilter(prop, beanClass)) { continue; } String fieldName = prop.getName(); Method readMethod = prop.getReadMethod(); if (readMethod == null) { continue; } Class fieldType = readMethod.getReturnType(); Field field = Classes.getField(fieldName, beanClass); if (field == null) { continue; } ApiParamObject param = toApiParam(field, fieldName, fieldType, defaultSeeClass); if (param == null) { continue; } result.add(param); } Collections.sort(result); return result; } ApiErrorObject getError(ApiError errorCode) { if (errorCode == null) { return null; } ApiErrorObject error = new ApiErrorObject(); String code = errorCode.value(); error.setId(code); error.setName(code); checkId(code); String comment = errorCode.comment(); if (comment == null) { comment = BusinessException.getMessage(code); } if (comment == null) { comment = ""; } error.setComment(comment); return error; } ApiParamObject toApiParam( AnnotatedElement element, String elementName, Class elementType, Class defaultSeeClass) { ApiParamObject apiParamObject = new ApiParamObject(); ApiParamLocation.collects(apiParamObject, element); String id = apiParamObject.getId(); checkId(id); String name = apiParamObject.getName(); if (StringUtils.isEmpty(name)) { name = elementName; } apiParamObject.setName(name); apiParamObject.setId(name); ApiComment apiComment = element.getAnnotation(ApiComment.class); ApiCommentUtils.setApiComment(apiComment, defaultSeeClass, apiParamObject); apiParamObject.setSourceType(elementType); ApiDataType dataType = convertType(elementType); apiParamObject.setDataType(dataType); return apiParamObject; } String[] combine(String[] classPaths, String[] methodPaths) { if (classPaths == null || classPaths.length == 0) { return methodPaths; } List paths = new ArrayList<>(); for (String basePath : classPaths) { for (String srcPath : methodPaths) { String path = basePath + srcPath; if (paths.contains(path)) { continue; } paths.add(path); } } return paths.toArray(new String[paths.size()]); } ApiDataType convertType(Class paramType) { if (paramType == null) { return null; } paramType = Classes.toWrapType(paramType); if (paramType.equals(Boolean.class)) { return ApiDataType.BOOLEAN; } if (paramType.equals(Integer.class) || paramType.equals(Short.class) || paramType.equals(Byte.class)) { return ApiDataType.INT; } // Date 类型返回为 long 格式。 ApiDataType dataType = DateConverter.dateAsLongType(paramType); if (dataType != null) { return dataType; } if (paramType.equals(Long.class)) { return ApiDataType.LONG; } if (paramType.equals(Float.class) || paramType.equals(Double.class)) { return ApiDataType.NUMBER; } if (paramType.equals(String.class) || paramType.equals(Character.class) || paramType.equals(StringBuffer.class) || paramType.equals(StringBuilder.class)) { return ApiDataType.STRING; } if (Classes.isInterface(paramType, Collection.class)) { return ApiDataType.ARRAY; } return ApiDataType.OBJECT; } private String[] getPath(RequestMapping mapping) { Set allPaths = new HashSet<>(); if (mapping != null) { String[] paths = mapping.path(); if (paths != null && paths.length > 0) { allPaths.addAll(Arrays.asList(paths)); } paths = mapping.value(); if (paths != null && paths.length > 0) { allPaths.addAll(Arrays.asList(paths)); } } return allPaths.toArray(new String[allPaths.size()]); } private void checkId(String id) { // TODO: CHECK id. } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocObjectFactory.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.Api2DocMocker; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.domain.ApiDataType; import com.terran4j.commons.api2doc.domain.DateConverter; import com.terran4j.commons.util.Classes; import com.terran4j.commons.util.Enums; import org.apache.commons.beanutils.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.beans.PropertyDescriptor; import java.lang.reflect.*; import java.util.*; /** * 根据类上的 Api2Doc 注解信息,创建 JavaBean、数组、列表等对象。 */ public class Api2DocObjectFactory { private static final Logger log = LoggerFactory.getLogger(Api2DocObjectFactory.class); public static Object createObject(ApiDataType dataType, Class elementType, String defaultValue) { if (dataType.isArrayType()) { int size = getArraySize(defaultValue, 1); return createList(elementType, size); } else if (dataType.isObjectType()) { return createBean(elementType); } else { if (DateConverter.isDateType(elementType)) { return DateConverter.dateAsLongValue(elementType); } if (StringUtils.hasText(defaultValue)) { return defaultValue; } else { return dataType.getDefault(); } } } public static T createBean(Class clazz) { Stack> classStack = new Stack>(); return createBean(clazz, null, classStack); } public static List createList(Class clazz, int size) { Stack> classStack = new Stack>(); return doCreateList(clazz, size, classStack); } public static T[] createArray(Class clazz, int size) { Stack> classStack = new Stack>(); return doCreateArray(clazz, size, classStack); } private static List doCreateList(Class clazz, int size, Stack> classStack) { List list = new ArrayList<>(); if (classStack.contains(clazz)) { return list; } for (int i = 0; i < size; i++) { E element = createBean(clazz, null, classStack); list.add(element); } return list; } private static T[] doCreateArray(Class clazz, int size, Stack> classStack) { if (size < 0) { return null; } T[] array = (T[]) Array.newInstance(clazz, size); if (classStack.contains(clazz)) { return array; } for (int i = 0; i < size; i++) { Object element = createBean(clazz, null, classStack); Array.set(array, i, element); } return array; } /** * 如果是 JavaBean 类,就创建这个 JavaBean 对象;
* 如果是 List / Array 对象,则什么都不做;
* 如果是 简单类型 对象,就创建符合这个类型的值。 * * @param clazz 数据对象的类型。 * @return 填充后的数据对象。 */ private static T createBean(Class clazz, String defaultValue, Stack> classStack) { if (clazz == null) { return null; } final ApiDataType dataType = ApiDataType.toDataType(clazz); if (dataType == null) { log.warn("无法识别的类型, class = {}", clazz); return null; } if (dataType.isArrayType()) { if (log.isInfoEnabled()) { log.info("数组类型,createBean 不处理, class = {}", clazz); } return null; } if (dataType.isSimpleType()) { if (defaultValue == null) { defaultValue = dataType.getDefault(); } Object result = dataType.parseValue(defaultValue); result = adaptSimpleType(result, clazz); return (T) result; } // 处理 JavaBean 对象的情况。 if (dataType.isObjectType()) { T object = null; try { object = clazz.newInstance(); } catch (InstantiationException e) { log.warn("不能根据类创建对象,class = {}, 原因:{}", clazz, e.getMessage()); return null; } catch (IllegalAccessException e) { log.warn("不能根据类创建对象,class = {}, 原因:{}", clazz, e.getMessage()); return null; } if (object == null) { log.warn("不能根据类创建对象,class = {}", clazz); return null; } // 获取 JavaBean 的属性。 PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(clazz); if (props == null || props.length == 0) { // 没有属性就不用处理。 return object; } // 之前有过此类的信息,不用再次输出。 if (classStack.contains(clazz)) { return object; } // 有属性,设置属性值。 classStack.push(clazz); for (PropertyDescriptor prop : props) { String fieldName = prop.getName(); try { fillField(fieldName, object, classStack); } catch (Exception e) { log.warn("给字段设值出错, class = {}, fieldName = {}", clazz, fieldName); } } classStack.pop(); return object; } log.warn("无法识别的类型,class = {}", clazz); return null; } private static void fillField(String name, Object bean, Stack> classStack) { Class clazz = Classes.getTargetClass(bean); PropertyDescriptor fieldProp = null; try { fieldProp = PropertyUtils.getPropertyDescriptor(bean, name); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } if (fieldProp == null) { throw new RuntimeException("field[" + name + "] NOT found in class: " + clazz); } if (Api2DocUtils.isFilter(fieldProp, clazz)) { return; } ApiComment classApiComment = clazz.getAnnotation(ApiComment.class); Class defaultSeeClass = ApiCommentUtils.getDefaultSeeClass( classApiComment, null); String fieldName = fieldProp.getName(); Method getMethod = fieldProp.getReadMethod(); if (getMethod == null) { log.warn("没有 getter 方法, class = {}, fieldName = {}", clazz, fieldName); return; } Method setMethod = fieldProp.getWriteMethod(); if (setMethod == null) { log.warn("没有 setter 方法, class = {}, fieldName = {}", clazz, fieldName); return; } Class fieldClass = getMethod.getReturnType(); Field field = Classes.getField(fieldName, clazz); if (field == null) { log.warn("找不到字段定义, class = {}, fieldName = {}", clazz, fieldName); return; } ApiDataType fieldDataType = ApiDataType.toDataType(fieldClass); if (fieldDataType == null) { log.warn("未知字段类型"); return; } Object fieldValue = null; if (fieldDataType.isSimpleType()) { ApiComment apiComment = field.getAnnotation(ApiComment.class); String defaultValue = ApiCommentUtils.getSample( apiComment, defaultSeeClass, fieldName); fieldValue = createBean(fieldClass, defaultValue, classStack); Class paramType = setMethod.getParameterTypes()[0]; fieldValue = adaptSimpleType(fieldValue, paramType); } else if (fieldDataType.isObjectType()) { fieldValue = createBean(fieldClass, null, classStack); } else if (fieldDataType.isArrayType()) { int size = 1; ApiComment apiComment = field.getAnnotation(ApiComment.class); if (apiComment != null) { String sizeText = ApiCommentUtils.getSample( apiComment, defaultSeeClass, field.getName()); size = getArraySize(sizeText, size); } Class elementClass = getArrayElementClass(field); if (fieldClass.isArray()) { fieldValue = doCreateArray(elementClass, size, classStack); } else if (List.class.equals(fieldClass)) { fieldValue = doCreateList(elementClass, size, classStack); } else { log.warn("不支持的集合类型,目前只支持 Array OR List, class = {}, fieldName = {}" + ", fieldClass = {}", clazz, fieldName, fieldClass); } } try { setMethod.invoke(bean, fieldValue); } catch (Exception e) { log.warn("调用 setter 方法失败, \n" + "clazz = {}, \n" + "setMethod = {}, \n" + "fieldValue = {}, \n" + "失败原因: {}", clazz, setMethod, fieldValue, e.getMessage()); } } private static int getArraySize(String sizeText, int defaultValue) { if (StringUtils.hasText(sizeText)) { try { return Integer.parseInt(sizeText); } catch (Exception e) { log.warn("List 或 Array 类型的字段上," + "@ApiComment 注解的 sample 属性应该是数字" + "(代表它在 mock 时元素的个数), sample = {}", sizeText); } } return defaultValue; } private static Object adaptSimpleType(Object sourceValue, Class targetType) { if (Long.class.equals(targetType) || long.class.equals(targetType)) { return Long.parseLong(sourceValue.toString()); } if (Byte.class.equals(targetType) || byte.class.equals(targetType)) { return Byte.parseByte(sourceValue.toString()); } if (Short.class.equals(targetType) || short.class.equals(targetType)) { return Short.parseShort(sourceValue.toString()); } if (Float.class.equals(targetType) || float.class.equals(targetType)) { return Float.parseFloat(sourceValue.toString()); } if (DateConverter.isDateType(targetType)) { return DateConverter.dateAsLongValue(targetType); } if (targetType.isEnum()) { return Enums.getEnumObject(targetType, sourceValue.toString()); } return sourceValue; } private static final Class getArrayElementClass(Field field) { Class returnType = field.getType(); if (returnType.isArray()) { Class elementClass = returnType.getComponentType(); return elementClass; } if (Classes.isInterface(returnType, Collection.class)) { Type type = field.getGenericType(); Type elementType = Api2DocUtils.getGenericType(type); if (elementType instanceof Class) { Class elementClass = (Class) elementType; return elementClass; } } log.warn("不支持的数组类型"); return null; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocProperties.java ================================================ package com.terran4j.commons.api2doc.impl; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class Api2DocProperties { @Value("${server.url:http://localhost:8080}") private String serverURL; @Value("${api2doc.showCurl:true}") private boolean showCurl = true; @Value("${service.name:}") private String serviceName; @Value("${api2doc.title:}") private String api2docTitle; @Value("${api2doc.icon:}") private String api2docIcon; public String getServerURL() { return serverURL; } public boolean isShowCurl() { return showCurl; } public String getServiceName() { return serviceName; } public String getApi2docTitle() { return api2docTitle; } public String getApi2docIcon() { return api2docIcon; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocService.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiFolderObject; import com.terran4j.commons.util.value.KeyedList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @Service public class Api2DocService { private static final Logger log = LoggerFactory.getLogger(Api2DocService.class); private static final String v = String.valueOf(System.currentTimeMillis()); private final KeyedList folders = new KeyedList<>(); public boolean hasFolder(String id) { return folders.containsKey(id); } public ApiFolderObject getFolder(String id) { return folders.get(id); } public void addFolder(ApiFolderObject folder) { if (folder == null) { throw new NullPointerException("ApiFolderObject is null"); } folders.add(folder.getId(), folder); } public List getFolders() { return folders.getAll(); } public String addAppDocVersion(String path) { if (path.indexOf("?") > 0) { return path + "&v=" + getAppDocVersion(); } else { return path + "?v=" + getAppDocVersion(); } } public String getAppDocVersion() { return v; } public String getComponentVersion() { return v; } public ApiDocObject getDocObject(String folderId, String docId) throws Exception { ApiFolderObject folder = getFolder(folderId); if (folder == null) { log.warn("ApiFolder NOT Found: {}", folderId); return null; } ApiDocObject doc = folder.getDoc(docId); if (doc == null) { if (log.isWarnEnabled()) { log.warn("ApiDoc NOT Found: {}", folderId); } return null; } return doc; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocUtils.java ================================================ package com.terran4j.commons.api2doc.impl; import com.fasterxml.jackson.annotation.JsonIgnore; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiObject; import com.terran4j.commons.api2doc.domain.ApiParamLocation; import com.terran4j.commons.api2doc.domain.ApiParamObject; import com.terran4j.commons.restpack.RestPackIgnore; import com.terran4j.commons.util.Classes; import com.terran4j.commons.util.Encoding; import com.terran4j.commons.util.Strings; import com.terran4j.commons.util.error.BusinessException; import com.terran4j.commons.util.value.ValueSource; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import java.beans.PropertyDescriptor; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URLEncoder; import java.util.*; public class Api2DocUtils { public static String toURL(ApiDocObject doc, String serverURL) { if (doc == null) { throw new NullPointerException("doc is null."); } boolean hasGetMethod = false; RequestMethod[] methods = doc.getMethods(); if (methods != null) { for (RequestMethod method : methods) { if (method == RequestMethod.GET) { hasGetMethod = true; } } } else { hasGetMethod = true; } if (!hasGetMethod) { // TODO: 暂时不支持非 GET 的请求。 return null; } String docURL = serverURL + doc.getPaths()[0]; List params = doc.getParams(); Map pathParams = new HashMap<>(); Map getParams = new HashMap<>(); if (params != null) { for (ApiParamObject param : params) { if (param.getLocation() == ApiParamLocation.PathVariable) { String value = param.getSample().getValue(); if (StringUtils.isEmpty(value)) { value = param.getDataType().getDefault(); } pathParams.put(param.getId(), value); } if (param.getLocation() == ApiParamLocation.RequestParam) { String value = param.getSample().getValue(); if (StringUtils.isEmpty(value)) { continue; } getParams.put(param.getId(), value); } } } if (pathParams.size() > 0) { docURL = Strings.format(docURL, new ValueSource() { @Override public String get(String key) { String value = pathParams.get(key); if (value == null) { return null; } return encode(value); } }, "{", "}", null); } if (getParams.size() > 0) { List keys = new ArrayList<>(); keys.addAll(getParams.keySet()); keys.sort(new Comparator() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); StringBuffer sb = new StringBuffer(); for (String key : keys) { String value = getParams.get(key); if (sb.length() > 0) { sb.append("&"); } sb.append(key).append("=").append(encode(value)); } docURL = docURL + "?" + sb.toString(); } return docURL; } private static String encode(String text) { try { return URLEncoder.encode(text, Encoding.UTF8.getName()); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } // public static final void setApiComment(ApiComment apiComment, ApiObject apiObject) { // if (apiComment != null && StringUtils.hasText(apiComment.value())) { // String commentText = getComment(apiComment); // apiObject.setComment(commentText); // } // if (apiComment != null && StringUtils.hasText(apiComment.sample())) { // if (StringUtils.hasText(apiComment.sample())) { // apiObject.setSample(apiComment.sample()); // } // } // } // // public static final String getComment(ApiComment apiComment) { // String comment = apiComment.value(); // if (StringUtils.isEmpty(comment)) { // return null; // } // return comment.trim(); // } // // public static final String getSample(ApiComment apiComment, Class clazz) throws BusinessException { // String sample = apiComment.sample(); // if (StringUtils.isEmpty(sample)) { // return null; // } // return sample.trim(); // //// if (!(sample.startsWith("@") && sample.endsWith("@"))) { //// return sample.replaceAll("\n", "
"); //// } //// //// String fileName = sample.substring(1, sample.length() - 1); //// String json = Strings.getString(clazz, fileName); //// if (StringUtils.isEmpty(json)) { //// throw new BusinessException(ErrorCodes.CONFIG_ERROR) //// .put("package", clazz.getPackage().getName()) //// .put("fileName", fileName) //// .setMessage("在包 ${package} 中找不到文件: ${fileName}"); //// } //// return json; // } public static final Class getArrayElementClass(Method method) { Class returnType = method.getReturnType(); if (returnType.isArray()) { Class elementClass = returnType.getComponentType(); return elementClass; } if (Classes.isInterface(returnType, Collection.class)) { Type gType = method.getGenericReturnType(); Type elementType = getGenericType(gType); if (elementType instanceof Class) { Class elementClass = (Class) elementType; return elementClass; } } return null; } public static final Type getGenericType(Type gType) { // 如果gType是泛型类型对像。 if (gType instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) gType; // 获得泛型类型的泛型参数 Type[] gArgs = pType.getActualTypeArguments(); return gArgs[gArgs.length - 1]; } else { System.out.println("获取泛型信息失败"); return null; } } public static final boolean isFilter( PropertyDescriptor prop, Class clazz) { String fieldName = prop.getName(); // class 只是 Java 对象自带的 Object.getClass() 方法,忽略掉。 if ("class".equals(fieldName)) { return true; } // 忽略掉需要忽略的字段。 Field field = Classes.getField(fieldName, clazz); if (field != null) { if (field.getAnnotation(RestPackIgnore.class) != null) { return true; } if (field.getAnnotation(JsonIgnore.class) != null) { return true; } } return false; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/ApiCommentUtils.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.annotations.ApiComment; import com.terran4j.commons.api2doc.domain.ApiObject; import com.terran4j.commons.util.Classes; import org.springframework.util.StringUtils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Stack; public class ApiCommentUtils { public static Class getDefaultSeeClass(ApiComment apiComment, Class previousSeeClass) { if (apiComment == null) { return previousSeeClass; } Class defaultSeeClass = apiComment.seeClass(); if (defaultSeeClass == Object.class) { return previousSeeClass; } return defaultSeeClass; } public static final void setApiComment(ApiComment apiComment, Class defaultSeeClass, ApiObject apiObject) { String comment = getComment(apiComment, defaultSeeClass, apiObject.getId()); if (comment != null) { apiObject.setComment(comment); } String sample = getSample(apiComment, defaultSeeClass, apiObject.getId()); if (sample != null) { apiObject.setSample(sample); } } // public static final ApiComment getComment(Field field, Class defaultSeeClass) { // ApiComment apiComment = field.getAnnotation(ApiComment.class); // if (apiComment !) // } public static final String getComment(ApiComment apiComment, Class defaultSeeClass, String defaultName) { if (apiComment != null) { String comment = apiComment.value(); if (StringUtils.hasText(comment)) { return comment.trim(); } } ApiComment seeComment = getSeeApiComment( apiComment, defaultSeeClass, defaultName); if (seeComment == null) { return null; } String comment = seeComment.value(); if (StringUtils.hasText(comment)) { return comment.trim(); } return null; } public static final String getSample(ApiComment apiComment, Class defaultSeeClass, String defaultName) { if (apiComment != null) { String sample = apiComment.sample(); if (StringUtils.hasText(sample)) { return sample.trim(); } } ApiComment seeComment = getSeeApiComment( apiComment, defaultSeeClass, defaultName); if (seeComment == null) { return null; } String sample = seeComment.sample(); if (StringUtils.hasText(sample)) { return sample.trim(); } return null; } /** * 获取可参照的 @ApiComment 注解对象。 * @param apiComment * @param defaultSeeClass * @param defaultSeeField * @return */ private static ApiComment getSeeApiComment( ApiComment apiComment, Class defaultSeeClass, String defaultSeeField) { Class seeClass = null; if (apiComment != null ) { seeClass = apiComment.seeClass(); } if (seeClass == null || seeClass == Object.class) { seeClass = defaultSeeClass; } if (seeClass == null || seeClass == Object.class) { return null; } String seeField = null; if (apiComment != null) { seeField = apiComment.seeField(); } if (StringUtils.isEmpty(seeField)) { seeField = defaultSeeField; } if (StringUtils.isEmpty(seeField)) { return null; } // 记录引用过的 seeClass, 用于循环引用检测。 List> path = new ArrayList<>(); Field field = null; ApiComment seeComment = null; while (seeClass != null) { // 循环引用检测。 if (path.contains(seeClass)) { StringBuffer sb = new StringBuffer(); sb.append("@ApiComment 中的 seeClass 不允许循环引用:"); for (int i = 0; i < path.size(); i++) { if (i > 0) { sb.append(" --> "); } sb.append(seeClass.getSimpleName()); } throw new RuntimeException(sb.toString()); } path.add(seeClass); // 寻找匹配字段中的 ApiComment 。 field = Classes.getField(seeField, seeClass); if (field != null) { seeComment = field.getAnnotation(ApiComment.class); if (seeComment != null) { return seeComment; } } ApiComment parentApiComment = seeClass.getAnnotation(ApiComment.class); if (parentApiComment == null) { break; } seeClass = parentApiComment.seeClass(); } return null; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/ClasspathFreeMarker.java ================================================ package com.terran4j.commons.api2doc.impl; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import com.terran4j.commons.util.Encoding; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; @Service public class ClasspathFreeMarker { private final Map templates = new HashMap(); private final Configuration freeMarker; public ClasspathFreeMarker() { super(); try { freeMarker = new Configuration(Configuration.VERSION_2_3_25); freeMarker.setDefaultEncoding(Encoding.UTF8.getName()); TemplateLoader ctl = new ClassTemplateLoader(getClass(), "/"); freeMarker.setTemplateLoader(ctl); } catch (Exception e) { throw new RuntimeException(e); } } private String getPath(Class clazz, String fileName) { String path = clazz.getPackage().getName().replace('.', '/') + "/" + fileName; return path; } public final Template getTemplate(Class clazz, String fileName) { String path = getPath(clazz, fileName); Template template = templates.get(path); if (template != null) { return template; } synchronized (ClasspathFreeMarker.class) { template = templates.get(path); if (template != null) { return template; } try { template = freeMarker.getTemplate(path); templates.put(path, template); return template; } catch (IOException e) { throw new RuntimeException(e); } } } public final String build(Template template, Map model) // throws IOException, TemplateException { if (!templates.containsValue(template)) { String msg = "Can't build from the tempate which NOT get by calling this method:\n" + "ClasspathFreeMarker.getTemplate(Class clazz, String fileName)"; throw new UnsupportedOperationException(msg); } String html = FreeMarkerTemplateUtils.processTemplateIntoString( // template, model); return html; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/CurlBuilder.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiParamLocation; import com.terran4j.commons.api2doc.domain.ApiParamObject; import com.terran4j.commons.util.Encoding; import com.terran4j.commons.util.Strings; import com.terran4j.commons.util.value.KeyedList; import com.terran4j.commons.util.value.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; public class CurlBuilder { private static final Logger log = LoggerFactory.getLogger(CurlBuilder.class); private static final String enter = " \\\n"; public static String toCurl(ApiDocObject docObject, String serverURL) { final List allParams = docObject.getParams(); final KeyedList headers = new KeyedList<>(); final KeyedList params = new KeyedList<>(); final KeyedList cookies = new KeyedList<>(); final Map pathVars = new HashMap<>(); final KeyedList parts = new KeyedList<>(); if (allParams.size() > 0) { for (ApiParamObject param : allParams) { String key = param.getId(); String value = param.getSample().getValue(); if (StringUtils.isEmpty(value)) { Class paramType = param.getSourceType(); if (paramType == String.class) { value = key; } else { value = param.getDataType().getDefault(); } } if (param.getLocation() == ApiParamLocation.RequestParam) { params.add(key, value); } if (param.getLocation() == ApiParamLocation.PathVariable) { pathVars.put(key, value); } if (param.getLocation() == ApiParamLocation.RequestHeader) { headers.add(key, value); } if (param.getLocation() == ApiParamLocation.CookieValue) { cookies.add(key, value); } if (param.getLocation() == ApiParamLocation.RequestPart) { parts.add(key, value); } } } RequestMethod[] requestMethods = docObject.getMethods(); RequestMethod requestMethod = requestMethods[0]; StringBuilder sb = new StringBuilder("curl"); if (parts.size() == 0) { // 没有上传文件,才指定方法。 sb.append(" -X ").append(requestMethod.name()); } sb.append(enter); // 将 Header 参数拼接起来。 if (headers.size() > 0) { for (int i = 0; i < headers.size(); i++) { String key = headers.getKey(i); String value = headers.get(i); sb.append(" -H \"").append(key).append(": ").append(value) .append("\"").append(enter); } } if (cookies.size() > 0) { sb.append(" -b \""); sb.append(joinText(cookies, ";", "=")); sb.append("\"").append(enter); } // 将 URL 中的 {xx} 变量用参数的示例值代替。 String url = serverURL + docObject.getPaths()[0]; if (pathVars.size() > 0) { ValueSource vars = new ValueSource() { @Override public String get(String key) { return encode(pathVars.get(key)); } }; url = Strings.format(url, vars, "{", "}", null); } // 将“参数”拼起来。 if (params.size() > 0) { if (parts.size() > 0) { // 参数按 multipart 的方法传。 for (int i = 0; i < params.size(); i++) { String key = params.getKey(i); String value = params.get(i); sb.append(" -F \"").append(key).append("=").append(value) .append("\"").append(enter); } } else if (requestMethod == RequestMethod.POST) { // 参数按 post 的方法传。 sb.append(" -d \"").append(joinText(params, "&", "=")) .append("\"").append(enter); } else { // 参数附加到 URL 后面。 url += ("?" + joinText(params, "&", "=")); } } // 追加 multipart 参数。 if (parts.size() > 0) { for (int i = 0; i < parts.size(); i++) { String key = parts.getKey(i); String value = parts.get(i); sb.append(" -F \"").append(key).append("=@").append(value) .append("\"").append(enter); } } // 将 URL 拼接起来。 sb.append(" \"").append(url).append("\""); String curl = sb.toString(); if (log.isInfoEnabled()) { log.info("doc[{}]'s curl:\n{}", docObject.getId(), curl); } return curl; } /** * 连接成字符串,并将 value 值进行 URL 编码。 */ public static final String joinText(KeyedList params, String joiner, String splitter) { StringBuffer sb = new StringBuffer(); boolean first = true; Iterator it = params.keySet().iterator(); while (it.hasNext()) { String key = it.next(); String value = params.get(key); if (!first) { sb.append(joiner); } sb.append(key).append(splitter).append(encode(value)); first = false; } return sb.toString(); } private static String encode(String value) { try { // 对于空格的编码,有的地方不认 + ,所以统一转成: %20 String str = URLEncoder.encode(value, Encoding.UTF8.getName()); return str.replaceAll("\\+", "%20"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/DocMenuBuilder.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.controller.MenuData; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiFolderObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @Service public class DocMenuBuilder { @Autowired private Api2DocService apiDocService; public List getMenuGroups() { List menuGroups = new ArrayList<>(); List folders = apiDocService.getFolders(); if (folders == null || folders.size() == 0) { return menuGroups; } for (ApiFolderObject folder : folders) { MenuData menuGroup = getMenuGroup(folder); menuGroups.add(menuGroup); } Collections.sort(menuGroups); return menuGroups; } public MenuData getMenuGroup(ApiFolderObject folder) { String folderId = folder.getId(); String folderName = folder.getName(); MenuData menuGroup = new MenuData(); menuGroup.setId(folderId); menuGroup.setIndex(folderId); menuGroup.setName(folderName); menuGroup.setFolder(true); menuGroup.setOrder(folder.getOrder()); List children = new ArrayList<>(); Map mds = folder.getMds(); if (mds != null && mds.size() > 0) { for (String md : mds.values()) { MenuData menu = getMenu(md, folderId); children.add(menu); } } List docs = folder.getDocs(); if (docs != null) { for (ApiDocObject doc : docs) { MenuData menu = getMenu(doc, folderId); children.add(menu); } } Collections.sort(children); menuGroup.setChildren(children); return menuGroup; } public MenuData getMenu(String mdFileName, String folderId) { int offset = mdFileName.indexOf("-"); String orderText = mdFileName.substring(0, offset); int order = Integer.parseInt(orderText); String docName = mdFileName.substring(offset + 1, mdFileName.length() - ".md".length()); String docId = ApiFolderObject.name2Id(mdFileName); MenuData menu = new MenuData(); String pageId = "md-" + folderId + "-" + docId; menu.setId(pageId); menu.setIndex(pageId); String url = getPageURL(pageId); menu.setUrl(url); menu.setFolder(false); menu.setName(docName); menu.setOrder(order); return menu; } public MenuData getMenu(ApiDocObject doc, String folderId) { MenuData menu = new MenuData(); String pageId = "api-" + folderId + "-" + doc.getId(); menu.setId(pageId); menu.setIndex(pageId); String url = getPageURL(pageId); menu.setUrl(url); menu.setFolder(false); menu.setName(doc.getName()); menu.setOrder(doc.getOrder()); return menu; } private String getPageURL(String pageId) { try { String p = URLEncoder.encode(pageId, "UTF-8"); String path = "/api2doc/home.html?p=" + p; return apiDocService.addAppDocVersion(path); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Can't encoding: " + pageId, e); } } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/DocPageBuilder.java ================================================ package com.terran4j.commons.api2doc.impl; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiFolderObject; import com.terran4j.commons.restpack.HttpResult; import com.terran4j.commons.restpack.config.RestPackConfiguration; import com.terran4j.commons.util.Strings; import com.vladsch.flexmark.ast.Node; import com.vladsch.flexmark.ext.spec.example.SpecExampleExtension; import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.parser.ParserEmulationProfile; import com.vladsch.flexmark.util.KeepType; import com.vladsch.flexmark.util.options.MutableDataSet; import freemarker.template.Template; import freemarker.template.TemplateException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.PostConstruct; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Service public class DocPageBuilder { private static final Logger log = LoggerFactory.getLogger(DocPageBuilder.class); private static final String FILE_DOC_MD = "doc.md.ftl"; @Autowired private Api2DocProperties api2DocProperties; @Autowired private Api2DocService apiDocService; @Autowired private ClasspathFreeMarker freeMarker; private Template mdTemplate = null; private Parser parser = null; private HtmlRenderer renderer = null; @PostConstruct public void init() { try { mdTemplate = freeMarker.getTemplate(DocPageBuilder.class, FILE_DOC_MD); MutableDataSet options = new MutableDataSet(); options.setFrom(ParserEmulationProfile.GITHUB_DOC); options.set(Parser.EXTENSIONS, Arrays.asList( TablesExtension.create(), // 表格渲染插件。 SpecExampleExtension.create() // 代码渲染插件。 )); // References compatibility options.set(Parser.REFERENCES_KEEP, KeepType.LAST); // Set GFM table parsing options options.set(TablesExtension.COLUMN_SPANS, false) // .set(TablesExtension.MIN_HEADER_ROWS, 1) // .set(TablesExtension.MAX_HEADER_ROWS, 1) // .set(TablesExtension.APPEND_MISSING_COLUMNS, true) // .set(TablesExtension.DISCARD_EXTRA_COLUMNS, true) // .set(TablesExtension.WITH_CAPTION, false) // .set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true); // Setup List Options for GitHub profile which is kramdown for documents options.setFrom(ParserEmulationProfile.GITHUB_DOC); // You can re-use parser and renderer instances parser = Parser.builder(options).build(); renderer = HtmlRenderer.builder(options).build(); } catch (Exception e) { throw new RuntimeException(e); } } public String md2Html(String md) throws Exception { Node document = parser.parse(md); String html = renderer.render(document); return html; } // public String md2HtmlPage(String md, String title) throws Exception { // String content = md2Html(md); // // Map model = new HashMap(); // if (title != null) { // model.put("title", title); // } // model.put("content", content); // model.put("v", apiDocService.getComponentVersion()); // // String html = freeMarker.build(docTemplate, model); // return html; // } // // public ApiDocObject getDocObject(String folderId, String docId) throws Exception { // ApiFolderObject folder = apiDocService.getFolder(folderId); // if (folder == null) { // log.warn("ApiFolder NOT Found: {}", folderId); // return null; // } // // ApiDocObject doc = folder.getDoc(docId); // if (doc == null) { // if (log.isWarnEnabled()) { // log.warn("ApiDoc NOT Found: {}", folderId); // } // return null; // } // // return doc; // } public String doc2Md(ApiDocObject doc) { if (doc == null) { return null; } ApiFolderObject folder = doc.getFolder(); try { Map model = new HashMap<>(); model.put("folder", folder); model.put("doc", doc); if (api2DocProperties.isShowCurl()) { String serverURL = api2DocProperties.getServerURL(); String curl = CurlBuilder.toCurl(doc, serverURL); if (StringUtils.hasText(curl)) { model.put("curl", curl); } } Object mockResult = doc.toMockResult(); if (folder.isRestPack()) { mockResult = HttpResult.successFully(mockResult); } if (mockResult != null) { String resultJson = RestPackConfiguration.getObjectMapper() .writeValueAsString(mockResult); if (StringUtils.hasText(resultJson)) { model.put("resultJson", resultJson); } } String folderId = folder.getId(); String upFirst = folderId.substring(0, 1).toUpperCase() + folderId.substring(1); String folderClasses = upFirst + "Service / " + upFirst + "Retrofit"; model.put("folderClasses", folderClasses); String content = freeMarker.build(mdTemplate, model); if (log.isInfoEnabled()) { log.info("\n{}", content); } return content; } catch (IOException | TemplateException e) { throw new RuntimeException(e); } } public String loadMdFromResource(String path) throws Exception { if (StringUtils.isEmpty(path)) { throw new NullPointerException("path is null."); } path = path.trim(); if (!path.startsWith("/")) { path = "/" + path; } path = "api2doc" + path; ClassLoader loader = Thread.currentThread().getContextClassLoader(); String md = Strings.getResourceByPath(path, loader); return md; } public String loadMdFromResource(String folderId, String docId) throws Exception { ApiFolderObject folder = apiDocService.getFolder(folderId); if (folder == null) { log.warn("ApiFolder NOT Found: {}", folderId); return null; } Map mds = folder.getMds(); if (mds == null || !mds.containsKey(docId)) { log.warn("Markdown doc {} NOT Found in Folder: {}", docId, folderId); return null; } String fileName = mds.get(docId); String path = folderId + "/" + fileName; String md = loadMdFromResource(path); return md; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/FlexibleString.java ================================================ package com.terran4j.commons.api2doc.impl; /** * 将空串视为 null * 能转成 html 格式。 */ public class FlexibleString { private StringBuilder value = new StringBuilder(); public FlexibleString() { } public FlexibleString(String value) { if (value != null) { this.value.append(value); } } public String getValue() { if (value.length() == 0) { return null; } return value.toString(); } public void setValue(String value) { if (value == null) { this.value = new StringBuilder(); } else { this.value = new StringBuilder(value); } } public FlexibleString append(String appendValue) { if (appendValue != null) { value.append(appendValue); } return this; } public FlexibleString insertLine(String insertValue) { if (insertValue != null) { if (this.value.length() > 0) { this.value.insert(0, insertValue + "\n"); } else { this.value.insert(0, insertValue); } } return this; } public String html() { if (value.length() == 0) { return null; } String replacement = "
"; return value.toString().replaceAll("\n", replacement); } public String javadoc(int indent) { if (value.length() == 0) { return null; } String replacement = ""; for (int i = 0; i < indent; i++) { replacement += " "; } replacement = "
\n" + replacement + (" * "); return value.toString().replaceAll("\n", replacement); } @Override public String toString() { if (value.length() == 0) { return ""; } return value.toString(); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/MappingMethod.java ================================================ package com.terran4j.commons.api2doc.impl; import org.springframework.web.bind.annotation.*; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.*; public class MappingMethod { /** * 目前只支持这 5 种方法,其它的都用得太少,暂时不支持。 */ static final RequestMethod[] SUPPORT_METHODS = new RequestMethod[] { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.PATCH }; private final Method method; private MappingMethod(Method method) { this.method = method; } public Method getMethod() { return method; } public String getName() { RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); if (requestMapping != null) { return requestMapping.name(); } GetMapping getMapping = method.getAnnotation(GetMapping.class); if (getMapping != null) { return getMapping.name(); } PostMapping postMapping = method.getAnnotation(PostMapping.class); if (postMapping != null) { return postMapping.name(); } PutMapping putMapping = method.getAnnotation(PutMapping.class); if (putMapping != null) { return putMapping.name(); } PatchMapping patchMapping = method.getAnnotation(PatchMapping.class); if (patchMapping != null) { return patchMapping.name(); } DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class); if (deleteMapping != null) { return deleteMapping.name(); } return null; } RequestMethod[] getRequestMethod() { RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); if (requestMapping != null) { RequestMethod[] methods = requestMapping.method(); if (methods == null || methods.length == 0) { return SUPPORT_METHODS; } return methods; } GetMapping getMapping = method.getAnnotation(GetMapping.class); if (getMapping != null) { return new RequestMethod[]{RequestMethod.GET}; } PostMapping postMapping = method.getAnnotation(PostMapping.class); if (postMapping != null) { return new RequestMethod[]{RequestMethod.POST}; } PutMapping putMapping = method.getAnnotation(PutMapping.class); if (putMapping != null) { return new RequestMethod[]{RequestMethod.PUT}; } PatchMapping patchMapping = method.getAnnotation(PatchMapping.class); if (patchMapping != null) { return new RequestMethod[]{RequestMethod.PATCH}; } DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class); if (deleteMapping != null) { return new RequestMethod[]{RequestMethod.DELETE}; } return SUPPORT_METHODS; } public String[] getPath() { RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); if (requestMapping != null) { return merge(requestMapping.path(), requestMapping.value()); } GetMapping getMapping = method.getAnnotation(GetMapping.class); if (getMapping != null) { return merge(getMapping.path(), getMapping.value()); } PostMapping postMapping = method.getAnnotation(PostMapping.class); if (postMapping != null) { return merge(postMapping.path(), postMapping.value()); } PutMapping putMapping = method.getAnnotation(PutMapping.class); if (putMapping != null) { return merge(putMapping.path(), putMapping.value()); } PatchMapping patchMapping = method.getAnnotation(PatchMapping.class); if (patchMapping != null) { return merge(patchMapping.path(), patchMapping.value()); } DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class); if (deleteMapping != null) { return merge(deleteMapping.path(), deleteMapping.value()); } return null; } private String[] merge(String[] strs1, String[] strs2) { Set strSet = new HashSet<>(); if (strs1 != null) { strSet.addAll(Arrays.asList(strs1)); } if (strs2 != null) { strSet.addAll(Arrays.asList(strs2)); } List list = new ArrayList<>(strSet); Collections.sort(list); return list.toArray(new String[list.size()]); } public static List getMappingMethods(Class clazz) { List mappingMethods = new ArrayList<>(); Method[] methods = clazz.getDeclaredMethods(); if (methods == null || methods.length == 0) { return mappingMethods; } for (Method method : methods) { if (isMappingMethod(method)) { MappingMethod mappingMethod = new MappingMethod(method); mappingMethods.add(mappingMethod); } } return mappingMethods; } public static boolean isMappingMethod(Method method) { Annotation mapping = method.getAnnotation(RequestMapping.class); if (mapping != null) { return true; } mapping = method.getAnnotation(GetMapping.class); if (mapping != null) { return true; } mapping = method.getAnnotation(PostMapping.class); if (mapping != null) { return true; } mapping = method.getAnnotation(PutMapping.class); if (mapping != null) { return true; } mapping = method.getAnnotation(PatchMapping.class); if (mapping != null) { return true; } mapping = method.getAnnotation(DeleteMapping.class); if (mapping != null) { return true; } return false; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ApiMetaService.java ================================================ package com.terran4j.commons.api2doc.meta; import com.terran4j.commons.api2doc.controller.ApiEntry; import com.terran4j.commons.api2doc.controller.ApiInfo; import com.terran4j.commons.api2doc.domain.ApiDocObject; import com.terran4j.commons.api2doc.domain.ApiFolderObject; import com.terran4j.commons.api2doc.domain.ApiParamLocation; import com.terran4j.commons.api2doc.domain.ApiParamObject; import com.terran4j.commons.api2doc.impl.Api2DocService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMethod; import java.util.ArrayList; import java.util.List; @Service public class ApiMetaService { @Autowired private Api2DocService api2DocService; public ApiInfo toApiInfo(String folderId, String docId) throws Exception { ApiDocObject doc = api2DocService.getDocObject(folderId, docId); ApiInfo apiInfo = new ApiInfo(); apiInfo.setUrl(doc.getPaths()[0]); apiInfo.setDefaultMethod(doc.getMethods()[0].name()); List apiParamObjects = doc.getParams(); if (apiParamObjects != null && apiParamObjects.size() > 0) { List params = new ArrayList<>(); List headers = new ArrayList<>(); for (ApiParamObject apiParamObject : apiParamObjects) { ApiEntry entry = new ApiEntry(); entry.setKey(apiParamObject.getId()); entry.setValue(apiParamObject.getSample().getValue()); ApiParamLocation paramLocation = apiParamObject.getLocation(); if (paramLocation == ApiParamLocation.RequestParam) { params.add(entry); } else if (paramLocation == ApiParamLocation.RequestHeader) { headers.add(entry); } } apiInfo.setParams(params); apiInfo.setHeaders(headers); } return apiInfo; } public List toClassMetaList() { List classes = new ArrayList<>(); List folderList = api2DocService.getFolders(); if (folderList == null || folderList.isEmpty()) { return classes; } for(ApiFolderObject folder : folderList) { ClassMeta classMeta = toClassMeta(folder); classes.add(classMeta); } return classes; } public ClassMeta toClassMeta(ApiFolderObject folder) { ClassMeta classMeta = new ClassMeta(); classMeta.setId(folder.getId()); classMeta.setName(folder.getName()); classMeta.setComment(folder.getComment().getValue()); List docs = folder.getDocs(); if (docs != null && docs.size() > 0) { for(ApiDocObject doc : docs) { MethodMeta methodMeta = toMethodMeta(doc); classMeta.addMethod(methodMeta); } } return classMeta; } public MethodMeta toMethodMeta(ApiDocObject doc) { MethodMeta methodMeta = new MethodMeta(); methodMeta.setId(doc.getId()); methodMeta.setComment(doc.getComment().getValue()); methodMeta.setName(doc.getName()); methodMeta.setPaths(doc.getPaths()); methodMeta.setRequestMethods(toRequestMethods(doc.getMethods())); List params = doc.getParams(); if (params != null && params.size() > 0) { for (ApiParamObject param : params) { ParamMeta paramMeta = toParamMeta(param); methodMeta.addParam(paramMeta); } } return methodMeta; } public ParamMeta toParamMeta(ApiParamObject param) { ParamMeta paramMeta = new ParamMeta(); paramMeta.setId(param.getId()); paramMeta.setComment(param.getComment().getValue()); paramMeta.setDataType(param.getDataType().getName()); paramMeta.setLocation(param.getLocation().name()); paramMeta.setName(param.getName()); paramMeta.setRequired(param.isRequired()); return paramMeta; } private String[] toRequestMethods(RequestMethod[] methods) { if (methods == null || methods.length == 0) { return new String[]{}; } String[] results = new String[methods.length]; for (int i = 0; i < methods.length; i++) { RequestMethod method = methods[i]; results[i] = method.name(); } return results; } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ClassMeta.java ================================================ package com.terran4j.commons.api2doc.meta; import java.util.ArrayList; import java.util.List; public class ClassMeta { private String id; private String name; private String comment; private final List methods = new ArrayList<>(); public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public List getMethods() { return methods; } public void addMethod(MethodMeta method) { this.methods.add(method); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/MethodMeta.java ================================================ package com.terran4j.commons.api2doc.meta; import java.util.ArrayList; import java.util.List; public class MethodMeta { private String id; private String name; private String comment; private String[] paths; private String[] requestMethods; private final List params = new ArrayList<>(); public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String[] getPaths() { return paths; } public void setPaths(String[] paths) { this.paths = paths; } public String[] getRequestMethods() { return requestMethods; } public void setRequestMethods(String[] requestMethods) { this.requestMethods = requestMethods; } public List getParams() { return params; } public void addParam(ParamMeta param) { this.params.add(param); } } ================================================ FILE: commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ParamMeta.java ================================================ package com.terran4j.commons.api2doc.meta; import com.terran4j.commons.api2doc.domain.ApiDataType; import com.terran4j.commons.api2doc.domain.ApiParamLocation; public class ParamMeta { private String id; private String name; private String comment; private boolean required; private String dataType; private String location; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public boolean isRequired() { return required; } public void setRequired(boolean required) { this.required = required; } public String getDataType() { return dataType; } public void setDataType(String dataType) { this.dataType = dataType; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } } ================================================ FILE: commons-api2doc/src/main/resources/api2doc/welcome.md ================================================ 欢迎使用 Api2Doc ! ================================================ FILE: commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/bean.java.ftl ================================================ <#if config.pkgName??> package ${config.pkgName}; <#list imports as import> import ${import}; /** <#if comment??> * ${comment}
* <#if config.declaredComment??> * ${config.declaredComment} */ public class ${class} { <#list fields as field> <#if field.comment ??> /** * ${field.comment} */ private ${field.type} ${field.name}; <#list fields as field> <#if field.comment ??> /** * @return ${field.comment} */ public ${field.type} ${field.getMethod}() { return ${field.name}; } public void ${field.setMethod}(${field.type} ${field.name}) { this.${field.name} = ${field.name}; } } ================================================ FILE: commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/enum.java.ftl ================================================ <#if config.pkgName??> package ${config.pkgName}; /** <#if config.declaredComment??> * ${config.declaredComment} */ public enum ${class} { <#list enums as enum> <#if enum.comment??> /** * ${enum.comment} */ ${enum.name}, } ================================================ FILE: commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/retrofit.java.ftl ================================================ <#if config.pkgName??> package ${config.pkgName}; import retrofit2.Call; import retrofit2.http.*; <#list imports as import> import ${import}; /** <#if comment??> * ${comment!} * <#if config.declaredComment??> * ${config.declaredComment} */ public interface ${class} { <#list methods as method> /** <#if method.comment??> * ${method.comment} <#if method.params??> <#list method.params as param> * @param ${param.id} ${param.comment!} <#if method.returnClass ??> * @return 返回由 ${method.returnClass} 对象序列化而成的 json 串。 */ <#if method.annos??> <#list method.annos as anno> ${anno} Call ${method.name}(<#list method.params as param>${param.expression}); } ================================================ FILE: commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/impl/doc.md.ftl ================================================ **API标识** - ${folder.id}-${doc.id} <#if doc.comment?? && doc.comment!=""> **API简介** - ${doc.comment}
<#--**客户端API类**--> <#--- ${folderClasses}--> <#--**客户端API方法**--> <#--- ${doc.id}--> <#--
--> **请求URI** <#list doc.paths as path> - `${path}` **请求方法** <#list doc.methods as method> - `${method}` <#if doc.params?? && (doc.params?size > 0) > **请求参数** | 参数名 | 是否必须 | 参数形式 | 数据类型 | 说明 | 示例值 | |:----- |----- |----- |----- |------ |----- | <#list doc.params as param> | ${param.id} | ${param.requiredName} | ${param.location} | ${param.typeName} | ${param.comment.html()!} | ${param.sample.html()!} | <#if curl??> **请求示例(curl 命令格式)** ``` ${curl} ```
<#if resultJson??> **返回数据示例** ```json ${resultJson} ``` <#if doc.results?? && (doc.results?size > 0) > **返回数据说明** - [${doc.returnTypeDesc}](#${doc.results[0].groupId}) (参见下面的类型说明) <#list doc.results as result> <#if result.groupId??>
**${result.groupName!}类型说明:** | 字段名 | 类型 | 说明 | 示例值 | |:---- |----- |----- |----- | <#list result.children as item> | ${item.id} | <#if item.refGroupId??>[${item.typeName}](#${item.refGroupId})<#else>${item.typeName} | ${item.comment.html()!} | ${item.sample.html()!} | <#elseif doc.resultType?? > **返回数据说明** | 类型 | 说明 | 示例值 | |:---- |----- |----- | | ${doc.resultType.typeName} | ${doc.resultType.comment.html()!} | ${doc.resultType.sample.html()!} |
<#if doc.errors?? && (doc.errors?size > 0) > **错误码** | 错误码 | 说明 | |:---- |----- | <#list doc.errors as error> | ${error.id} | ${error.comment.html()!} | ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/css/doc.less ================================================ ////////////////////////// 整个 html、 body 及公共的样式 ///////////////// html { overflow-y: scroll; } @media only screen and (min-width: 48rem) { body { font-size: 1.4rem; } } @media only screen and (min-width: 76.8rem) { body { font-size: 1.6rem; } } //////////////////////////// 文档标题及内容块的样式 /////////////////////// .title { color: rgb(51, 51, 51); font-family: "Microsoft Yahei, 微软雅黑, Tahoma, Arial, Helvetica, STHeiti"; font-size: 2.4rem; font-weight: bold; text-align: left; text-rendering: optimizeLegibility; text-size-adjust: 100%; height: 4rem; line-height: 4rem; margin-left: 3rem; padding-bottom: 0.5rem; padding-top: 1rem; } .content { padding-top: 1rem; word-break: break-all; overflow-x: hidden; overflow-y: hidden; font-size: 1.6rem; line-height: 1.7rem; margin-left: 3rem; } .test-entry { float: right; margin-top: 1rem; margin-right: 3rem; } .clear { clear: both; } ////////////////////////////// 自定义滚动条样式 ///////////////// ::-webkit-scrollbar { /* 整个滚条背景 */ width: 0.5rem; height: 0.5rem; } ::-webkit-scrollbar-thumb { // 滚动条滑块(是主要部分) background-color: #c2c2c2; /* 滚条内嵌颜色 */ background-clip: padding-box; min-height: 3rem; } ::-webkit-scrollbar-track { // 外层轨道。 background-color: #FFF; } ::-webkit-scrollbar-button { // 不要两端的按钮。 display: none; } ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/css/home.less ================================================ // 菜单栏背景的颜色。 @menu-color: white; // 分隔线的颜色: @split-color: rgba(0,0,0,.07); html { overflow: hidden; } .doc-app { width: 99.52%; margin: 0px; padding: 0px; } .doc-top { width: 100%; height: 5rem; border-bottom: 1px solid rgba(0,0,0,.07); } .doc-icon { float: left; position: relative; text-align: center; margin-left: 2rem; margin-top: 0.7rem; background-position: center center; } .doc-icon-img { width: 3rem; height: 3rem; cursor: pointer; } .doc-title { float: left; position: relative; font-size: 2rem; font-weight: bold; line-height: 4rem; height: 4rem; margin-left: 2rem; text-align: center; vertical-align: middle; overflow: hidden; } .doc-body, .doc-left, .doc-middle { } .doc-body { width: 100%; position: absolute; top: 5rem; bottom: 0px; left: 0px; margin-top: 0.673rem; } .doc-left { width: 20%; height: 100%; overflow-x: hidden; overflow-y: auto; float: left; position: relative; display: block; background-color: @menu-color; } #doc-menus { width: 100%; margin-top: 1rem; } .doc-content { width: 79%; height: 100%; float: left; overflow: hidden; border-left: 1px solid @split-color; } .doc-frame { width: 100%; height: 100%; } .doc-end { clear: both; display: none; } ////////////////////// 重新定义菜单栏的颜色 //////////////////// .el-menu { background-color: @menu-color; } .el-submenu .el-menu { background-color: @menu-color; } ////////////////////////////// 自定义滚动条样式 ///////////////// ::-webkit-scrollbar { /* 整个滚条背景 */ width: 0.5rem; height: 0.5rem; } ::-webkit-scrollbar-thumb { // 滚动条滑块(是主要部分) background-color: #c2c2c2; /* 滚条内嵌颜色 */ background-clip: padding-box; min-height: 3rem; } ::-webkit-scrollbar-track { // 外层轨道。 background-color: #FFF; } ::-webkit-scrollbar-button { // 不要两端的按钮。 display: none; } ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/css/md.less ================================================ //////////////////////////////// 通用样式 ////////////////////// p, blockquote, ul, ol, dl, li, table, pre { margin: 1.5rem 0; } //////////////////////////////// 各种型号的标题的样式 //////////////////// h1, h2, h3, h4, h5, h6 { font-weight: bold; padding-bottom: 1rem; } h1 { color: #000000; font-size: 2.8rem; } h2 { color: #000000; font-size: 2.4rem; } h3 { font-size: 1.8rem; } h4 { font-size: 1.6rem; } h5 { font-size: 1.4rem; } h6 { color: #777777; background-color: inherit; font-size: 1.4rem; } hr { height: 0.2em; border: 0; color: #CCCCCC; background-color: #CCCCCC; } ////////////////////////////// 段落的样式 ////////////////////// p { margin: 1rem 0; } ////////////////////////////// 代码块的样式 ////////////////////// pre { overflow: auto; width: 94.5%; padding: 0.5rem; white-space: pre-wrap; /*css-3*/ white-space: -moz-pre-wrap; /*Mozilla,since1999*/ white-space: -o-pre-wrap; /*Opera7*/ word-wrap: break-word; /*InternetExplorer5.5+*/ } code { font-family: Consolas, Monaco, Andale Mono, monospace; background-color: #F8F8F8; border: 1px solid #CCCCCC; border-radius: 0.3rem; padding: 0 0.2em; line-height: 1; color: #d14; white-space: pre-wrap; } /////////////////////////// 链接的样式 //////////////////////////////////// a { color: #0645ad; text-decoration: none; } a:visited { color: #0b0080; } a:hover { color: #06e; } a:active { color: #faa700; } a:focus { outline: thin dotted; } a:hover, a:active { outline: 0; } ///////////////////////////// 被选中的文本的样式 /////////////////////// ::-moz-selection { background: rgba(255, 255, 0, 0.3); color: #000 } ::selection { background: rgba(255, 255, 0, 0.3); color: #000 } a::-moz-selection { background: rgba(255, 255, 0, 0.3); color: #0645ad } a::selection { background: rgba(255, 255, 0, 0.3); color: #0645ad } /////////////////////////////// 条目的样式 ///////////////////// blockquote { color: #666666; margin: 0; padding-left: 3em; border-left: 0.5em #EEE solid; } ul, ol { margin: 1em 0; padding: 0 0 0 2em; } li p:last-child { margin: 0 } dd { margin: 0 0 0 2em; } ///////////////////////// 图片的样式 //////////////////////// img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; max-width: 100%; } ///////////////////////// 表格的样式 //////////////////////// table { border-collapse: collapse; border-spacing: 0; width: 95%; } tr { color: rgb(255, 255, 255); } th, td { vertical-align: top; display: table-cell; padding: 0.8rem; line-height: 2rem; text-align: left; vertical-align: top; color: rgb(51, 51, 51); border-top: 1px solid #ddd; border-left: 1px solid #ddd; } thead tr { background-color: rgb(0, 136, 204); } thead tr th { text-align: left; min-width: 7.7rem; vertical-align: bottom; font-weight: bold; } tbody { display: table-row-group; vertical-align: middle; border-color: inherit; border-bottom: 1px solid #ddd; border-right: 1px solid #ddd; } tbody tr th { text-align: left; border-bottom-left-radius: 0.4rem; color: #333; } ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/css/test.less ================================================ @left-space: 1rem; @font-base-size: 1rem; h2 { margin-left: @left-space; } //h3 { // margin-left: @left-space; //} .clear { clear: both; } ////////////////////////////////////////////////////////////////// .items { margin-top: 2rem; margin-left: @left-space; } .items-desc { font-size: @font-base-size; margin-left: @left-space; margin-bottom: 0.5rem; } .item-list { float: left; width: 95%; } .item { margin-bottom: 0.5rem; margin-right: 2rem; width: 45%; float: left; } .item-key { width: 25%; margin-left: @left-space; float: left; } .item-split { margin-top: 0.7rem; margin-left: 0.3rem; font-size: @font-base-size; float: left; } .item-value { width: 65%; margin-left: 0.1rem; float: left; } .item-new { margin-left: @left-space; float: left; clear: both; } //////////////////////////////////////////////////////////////////////// .request { width: 95%; margin-left: @left-space; } .request-method { width: 10%; margin-left: @left-space; float: left; } .request-url { width: 77.5%; margin-left: 1rem; float: left; } ////////////////////////////////////////////////////////////////// .send { margin-left: @left-space; width: 95%; } .send-button { float: right; } ///////////////////////////////////////////////////////////////////////// .headers { margin-top: 2rem; margin-left: @left-space; } ////////////////////////////////////////////////////////////////////////// .response { margin-left: @left-space; margin-top: 4rem; width: 95%; } .response-code { margin-left: @left-space; } /////////////////////////////////////////////////////////////////////// .output { margin-left: @left-space; width: 95%; } pre.output-logs { margin-left: @left-space; white-space: pre-wrap; word-wrap: break-word; background-color: #F0F0F0; } ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/flexible-lite/flexible-lite-1.0.js ================================================ /** * @param designWidth: 设计稿的实际宽度值,需要根据实际设置 */ function flex(designWidth) { var doc = document, win = window, docEl = doc.documentElement, remStyle = document.createElement("style"), tid; // setTimeout 的句柄。 function refreshRem() { var width = docEl.getBoundingClientRect().width; var rem = width * 10 / designWidth; remStyle.innerHTML = 'html{font-size:' + rem + 'px;}'; } if (docEl.firstElementChild) { docEl.firstElementChild.appendChild(remStyle); } else { var wrap = doc.createElement("div"); wrap.appendChild(remStyle); doc.write(wrap.innerHTML); wrap = null; } //要等 viewport 设置好后才能执行 refreshRem,不然 refreshRem 会执行2次; refreshRem(); win.addEventListener("resize", function () { clearTimeout(tid); //防止执行两次 tid = setTimeout(refreshRem, 300); }, false); win.addEventListener("pageshow", function (e) { if (e.persisted) { // 浏览器后退的时候重新计算 clearTimeout(tid); tid = setTimeout(refreshRem, 300); } }, false); if (doc.readyState === "complete") { doc.body.style.fontSize = "16px"; } else { doc.addEventListener("DOMContentLoaded", function (e) { doc.body.style.fontSize = "16px"; }, false); } } ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/less/less-1.7.0.js ================================================ /*! * LESS - Leaner CSS v1.7.0 * http://lesscss.org * * Copyright (c) 2009-2014, Alexis Sellier * Licensed under the Apache v2 License. * */ /** * @license Apache v2 */ (function (window, undefined) {// // Stub out `require` in the browser // function require(arg) { return window.less[arg.split('/')[1]]; }; if (typeof(window.less) === 'undefined' || typeof(window.less.nodeType) !== 'undefined') { window.less = {}; } less = window.less; tree = window.less.tree = {}; less.mode = 'browser'; var less, tree; // Node.js does not have a header file added which defines less if (less === undefined) { less = exports; tree = require('./tree'); less.mode = 'node'; } // // less.js - parser // // A relatively straight-forward predictive parser. // There is no tokenization/lexing stage, the input is parsed // in one sweep. // // To make the parser fast enough to run in the browser, several // optimization had to be made: // // - Matching and slicing on a huge input is often cause of slowdowns. // The solution is to chunkify the input into smaller strings. // The chunks are stored in the `chunks` var, // `j` holds the current chunk index, and `currentPos` holds // the index of the current chunk in relation to `input`. // This gives us an almost 4x speed-up. // // - In many cases, we don't need to match individual tokens; // for example, if a value doesn't hold any variables, operations // or dynamic references, the parser can effectively 'skip' it, // treating it as a literal. // An example would be '1px solid #000' - which evaluates to itself, // we don't need to know what the individual components are. // The drawback, of course is that you don't get the benefits of // syntax-checking on the CSS. This gives us a 50% speed-up in the parser, // and a smaller speed-up in the code-gen. // // // Token matching is done with the `$` function, which either takes // a terminal string or regexp, or a non-terminal function to call. // It also takes care of moving all the indices forwards. // // less.Parser = function Parser(env) { var input, // LeSS input string i, // current index in `input` j, // current chunk saveStack = [], // holds state for backtracking furthest, // furthest index the parser has gone to chunks, // chunkified input current, // current chunk currentPos, // index of current chunk, in `input` parser, parsers, rootFilename = env && env.filename; // Top parser on an import tree must be sure there is one "env" // which will then be passed around by reference. if (!(env instanceof tree.parseEnv)) { env = new tree.parseEnv(env); } var imports = this.imports = { paths: env.paths || [], // Search paths, when importing queue: [], // Files which haven't been imported yet files: env.files, // Holds the imported parse trees contents: env.contents, // Holds the imported file contents contentsIgnoredChars: env.contentsIgnoredChars, // lines inserted, not in the original less mime: env.mime, // MIME type of .less files error: null, // Error in parsing/evaluating an import push: function (path, currentFileInfo, importOptions, callback) { var parserImports = this; this.queue.push(path); var fileParsedFunc = function (e, root, fullPath) { parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue var importedPreviously = fullPath === rootFilename; parserImports.files[fullPath] = root; // Store the root if (e && !parserImports.error) { parserImports.error = e; } callback(e, root, importedPreviously, fullPath); }; if (less.Parser.importer) { less.Parser.importer(path, currentFileInfo, fileParsedFunc, env); } else { less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) { if (e) {fileParsedFunc(e); return;} var newEnv = new tree.parseEnv(env); newEnv.currentFileInfo = newFileInfo; newEnv.processImports = false; newEnv.contents[fullPath] = contents; if (currentFileInfo.reference || importOptions.reference) { newFileInfo.reference = true; } if (importOptions.inline) { fileParsedFunc(null, contents, fullPath); } else { new(less.Parser)(newEnv).parse(contents, function (e, root) { fileParsedFunc(e, root, fullPath); }); } }, env); } } }; function save() { currentPos = i; saveStack.push( { current: current, i: i, j: j }); } function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; } function forget() { saveStack.pop(); } function sync() { if (i > currentPos) { current = current.slice(i - currentPos); currentPos = i; } } function isWhitespace(str, pos) { var code = str.charCodeAt(pos | 0); return (code <= 32) && (code === 32 || code === 10 || code === 9); } // // Parse from a token, regexp or string, and move forward if match // function $(tok) { var tokType = typeof tok, match, length; // Either match a single character in the input, // or match a regexp in the current chunk (`current`). // if (tokType === "string") { if (input.charAt(i) !== tok) { return null; } skipWhitespace(1); return tok; } // regexp sync (); if (! (match = tok.exec(current))) { return null; } length = match[0].length; // The match is confirmed, add the match length to `i`, // and consume any extra white-space characters (' ' || '\n') // which come after that. The reason for this is that LeSS's // grammar is mostly white-space insensitive. // skipWhitespace(length); if(typeof(match) === 'string') { return match; } else { return match.length === 1 ? match[0] : match; } } // Specialization of $(tok) function $re(tok) { if (i > currentPos) { current = current.slice(i - currentPos); currentPos = i; } var m = tok.exec(current); if (!m) { return null; } skipWhitespace(m[0].length); if(typeof m === "string") { return m; } return m.length === 1 ? m[0] : m; } var _$re = $re; // Specialization of $(tok) function $char(tok) { if (input.charAt(i) !== tok) { return null; } skipWhitespace(1); return tok; } function skipWhitespace(length) { var oldi = i, oldj = j, curr = i - currentPos, endIndex = i + current.length - curr, mem = (i += length), inp = input, c; for (; i < endIndex; i++) { c = inp.charCodeAt(i); if (c > 32) { break; } if ((c !== 32) && (c !== 10) && (c !== 9) && (c !== 13)) { break; } } current = current.slice(length + i - mem + curr); currentPos = i; if (!current.length && (j < chunks.length - 1)) { current = chunks[++j]; skipWhitespace(0); // skip space at the beginning of a chunk return true; // things changed } return oldi !== i || oldj !== j; } function expect(arg, msg) { // some older browsers return typeof 'function' for RegExp var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg); if (result) { return result; } error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" : "unexpected token")); } // Specialization of expect() function expectChar(arg, msg) { if (input.charAt(i) === arg) { skipWhitespace(1); return arg; } error(msg || "expected '" + arg + "' got '" + input.charAt(i) + "'"); } function error(msg, type) { var e = new Error(msg); e.index = i; e.type = type || 'Syntax'; throw e; } // Same as $(), but don't change the state of the parser, // just return the match. function peek(tok) { if (typeof(tok) === 'string') { return input.charAt(i) === tok; } else { return tok.test(current); } } // Specialization of peek() function peekChar(tok) { return input.charAt(i) === tok; } function getInput(e, env) { if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) { return parser.imports.contents[e.filename]; } else { return input; } } function getLocation(index, inputStream) { var n = index + 1, line = null, column = -1; while (--n >= 0 && inputStream.charAt(n) !== '\n') { column++; } if (typeof index === 'number') { line = (inputStream.slice(0, index).match(/\n/g) || "").length; } return { line: line, column: column }; } function getDebugInfo(index, inputStream, env) { var filename = env.currentFileInfo.filename; if(less.mode !== 'browser' && less.mode !== 'rhino') { filename = require('path').resolve(filename); } return { lineNumber: getLocation(index, inputStream).line + 1, fileName: filename }; } function LessError(e, env) { var input = getInput(e, env), loc = getLocation(e.index, input), line = loc.line, col = loc.column, callLine = e.call && getLocation(e.call, input).line, lines = input.split('\n'); this.type = e.type || 'Syntax'; this.message = e.message; this.filename = e.filename || env.currentFileInfo.filename; this.index = e.index; this.line = typeof(line) === 'number' ? line + 1 : null; this.callLine = callLine + 1; this.callExtract = lines[callLine]; this.stack = e.stack; this.column = col; this.extract = [ lines[line - 1], lines[line], lines[line + 1] ]; } LessError.prototype = new Error(); LessError.prototype.constructor = LessError; this.env = env = env || {}; // The optimization level dictates the thoroughness of the parser, // the lower the number, the less nodes it will create in the tree. // This could matter for debugging, or if you want to access // the individual nodes in the tree. this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; // // The Parser // parser = { imports: imports, // // Parse an input string into an abstract syntax tree, // @param str A string containing 'less' markup // @param callback call `callback` when done. // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply // parse: function (str, callback, additionalData) { var root, line, lines, error = null, globalVars, modifyVars, preText = ""; i = j = currentPos = furthest = 0; globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\n' : ''; modifyVars = (additionalData && additionalData.modifyVars) ? '\n' + less.Parser.serializeVars(additionalData.modifyVars) : ''; if (globalVars || (additionalData && additionalData.banner)) { preText = ((additionalData && additionalData.banner) ? additionalData.banner : "") + globalVars; parser.imports.contentsIgnoredChars[env.currentFileInfo.filename] = preText.length; } str = str.replace(/\r\n/g, '\n'); // Remove potential UTF Byte Order Mark input = str = preText + str.replace(/^\uFEFF/, '') + modifyVars; parser.imports.contents[env.currentFileInfo.filename] = str; // Split the input into chunks. chunks = (function (input) { var len = input.length, level = 0, parenLevel = 0, lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace, chunks = [], emitFrom = 0, parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched; function fail(msg, index) { error = new(LessError)({ index: index || parserCurrentIndex, type: 'Parse', message: msg, filename: env.currentFileInfo.filename }, env); } function emitChunk(force) { var len = parserCurrentIndex - emitFrom; if (((len < 512) && !force) || !len) { return; } chunks.push(input.slice(emitFrom, parserCurrentIndex + 1)); emitFrom = parserCurrentIndex + 1; } for (parserCurrentIndex = 0; parserCurrentIndex < len; parserCurrentIndex++) { cc = input.charCodeAt(parserCurrentIndex); if (((cc >= 97) && (cc <= 122)) || (cc < 34)) { // a-z or whitespace continue; } switch (cc) { case 40: // ( parenLevel++; lastOpeningParen = parserCurrentIndex; continue; case 41: // ) if (--parenLevel < 0) { return fail("missing opening `(`"); } continue; case 59: // ; if (!parenLevel) { emitChunk(); } continue; case 123: // { level++; lastOpening = parserCurrentIndex; continue; case 125: // } if (--level < 0) { return fail("missing opening `{`"); } if (!level && !parenLevel) { emitChunk(); } continue; case 92: // \ if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; } return fail("unescaped `\\`"); case 34: case 39: case 96: // ", ' and ` matched = 0; currentChunkStartIndex = parserCurrentIndex; for (parserCurrentIndex = parserCurrentIndex + 1; parserCurrentIndex < len; parserCurrentIndex++) { cc2 = input.charCodeAt(parserCurrentIndex); if (cc2 > 96) { continue; } if (cc2 == cc) { matched = 1; break; } if (cc2 == 92) { // \ if (parserCurrentIndex == len - 1) { return fail("unescaped `\\`"); } parserCurrentIndex++; } } if (matched) { continue; } return fail("unmatched `" + String.fromCharCode(cc) + "`", currentChunkStartIndex); case 47: // /, check for comment if (parenLevel || (parserCurrentIndex == len - 1)) { continue; } cc2 = input.charCodeAt(parserCurrentIndex + 1); if (cc2 == 47) { // //, find lnfeed for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len; parserCurrentIndex++) { cc2 = input.charCodeAt(parserCurrentIndex); if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; } } } else if (cc2 == 42) { // /*, find */ lastMultiComment = currentChunkStartIndex = parserCurrentIndex; for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len - 1; parserCurrentIndex++) { cc2 = input.charCodeAt(parserCurrentIndex); if (cc2 == 125) { lastMultiCommentEndBrace = parserCurrentIndex; } if (cc2 != 42) { continue; } if (input.charCodeAt(parserCurrentIndex + 1) == 47) { break; } } if (parserCurrentIndex == len - 1) { return fail("missing closing `*/`", currentChunkStartIndex); } parserCurrentIndex++; } continue; case 42: // *, check for unmatched */ if ((parserCurrentIndex < len - 1) && (input.charCodeAt(parserCurrentIndex + 1) == 47)) { return fail("unmatched `/*`"); } continue; } } if (level !== 0) { if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) { return fail("missing closing `}` or `*/`", lastOpening); } else { return fail("missing closing `}`", lastOpening); } } else if (parenLevel !== 0) { return fail("missing closing `)`", lastOpeningParen); } emitChunk(true); return chunks; })(str); if (error) { return callback(new(LessError)(error, env)); } current = chunks[0]; // Start with the primary rule. // The whole syntax tree is held under a Ruleset node, // with the `root` property set to true, so no `{}` are // output. The callback is called when the input is parsed. try { root = new(tree.Ruleset)(null, this.parsers.primary()); root.root = true; root.firstRoot = true; } catch (e) { return callback(new(LessError)(e, env)); } root.toCSS = (function (evaluate) { return function (options, variables) { options = options || {}; var evaldRoot, css, evalEnv = new tree.evalEnv(options); // // Allows setting variables with a hash, so: // // `{ color: new(tree.Color)('#f01') }` will become: // // new(tree.Rule)('@color', // new(tree.Value)([ // new(tree.Expression)([ // new(tree.Color)('#f01') // ]) // ]) // ) // if (typeof(variables) === 'object' && !Array.isArray(variables)) { variables = Object.keys(variables).map(function (k) { var value = variables[k]; if (! (value instanceof tree.Value)) { if (! (value instanceof tree.Expression)) { value = new(tree.Expression)([value]); } value = new(tree.Value)([value]); } return new(tree.Rule)('@' + k, value, false, null, 0); }); evalEnv.frames = [new(tree.Ruleset)(null, variables)]; } try { var preEvalVisitors = [], visitors = [ new(tree.joinSelectorVisitor)(), new(tree.processExtendsVisitor)(), new(tree.toCSSVisitor)({compress: Boolean(options.compress)}) ], i, root = this; if (options.plugins) { for(i =0; i < options.plugins.length; i++) { if (options.plugins[i].isPreEvalVisitor) { preEvalVisitors.push(options.plugins[i]); } else { if (options.plugins[i].isPreVisitor) { visitors.splice(0, 0, options.plugins[i]); } else { visitors.push(options.plugins[i]); } } } } for(i = 0; i < preEvalVisitors.length; i++) { preEvalVisitors[i].run(root); } evaldRoot = evaluate.call(root, evalEnv); for(i = 0; i < visitors.length; i++) { visitors[i].run(evaldRoot); } if (options.sourceMap) { evaldRoot = new tree.sourceMapOutput( { contentsIgnoredCharsMap: parser.imports.contentsIgnoredChars, writeSourceMap: options.writeSourceMap, rootNode: evaldRoot, contentsMap: parser.imports.contents, sourceMapFilename: options.sourceMapFilename, sourceMapURL: options.sourceMapURL, outputFilename: options.sourceMapOutputFilename, sourceMapBasepath: options.sourceMapBasepath, sourceMapRootpath: options.sourceMapRootpath, outputSourceFiles: options.outputSourceFiles, sourceMapGenerator: options.sourceMapGenerator }); } css = evaldRoot.toCSS({ compress: Boolean(options.compress), dumpLineNumbers: env.dumpLineNumbers, strictUnits: Boolean(options.strictUnits), numPrecision: 8}); } catch (e) { throw new(LessError)(e, env); } if (options.cleancss && less.mode === 'node') { var CleanCSS = require('clean-css'), cleancssOptions = options.cleancssOptions || {}; if (cleancssOptions.keepSpecialComments === undefined) { cleancssOptions.keepSpecialComments = "*"; } cleancssOptions.processImport = false; cleancssOptions.noRebase = true; if (cleancssOptions.noAdvanced === undefined) { cleancssOptions.noAdvanced = true; } return new CleanCSS(cleancssOptions).minify(css); } else if (options.compress) { return css.replace(/(^(\s)+)|((\s)+$)/g, ""); } else { return css; } }; })(root.eval); // If `i` is smaller than the `input.length - 1`, // it means the parser wasn't able to parse the whole // string, so we've got a parsing error. // // We try to extract a \n delimited string, // showing the line where the parse error occured. // We split it up into two parts (the part which parsed, // and the part which didn't), so we can color them differently. if (i < input.length - 1) { i = furthest; var loc = getLocation(i, input); lines = input.split('\n'); line = loc.line + 1; error = { type: "Parse", message: "Unrecognised input", index: i, filename: env.currentFileInfo.filename, line: line, column: loc.column, extract: [ lines[line - 2], lines[line - 1], lines[line] ] }; } var finish = function (e) { e = error || e || parser.imports.error; if (e) { if (!(e instanceof LessError)) { e = new(LessError)(e, env); } return callback(e); } else { return callback(null, root); } }; if (env.processImports !== false) { new tree.importVisitor(this.imports, finish) .run(root); } else { return finish(); } }, // // Here in, the parsing rules/functions // // The basic structure of the syntax tree generated is as follows: // // Ruleset -> Rule -> Value -> Expression -> Entity // // Here's some LESS code: // // .class { // color: #fff; // border: 1px solid #000; // width: @w + 4px; // > .child {...} // } // // And here's what the parse tree might look like: // // Ruleset (Selector '.class', [ // Rule ("color", Value ([Expression [Color #fff]])) // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) // Ruleset (Selector [Element '>', '.child'], [...]) // ]) // // In general, most rules will try to parse a token with the `$()` function, and if the return // value is truly, will return a new node, of the relevant type. Sometimes, we need to check // first, before parsing, that's when we use `peek()`. // parsers: parsers = { // // The `primary` rule is the *entry* and *exit* point of the parser. // The rules here can appear at any level of the parse tree. // // The recursive nature of the grammar is an interplay between the `block` // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, // as represented by this simplified grammar: // // primary → (ruleset | rule)+ // ruleset → selector+ block // block → '{' primary '}' // // Only at one point is the primary rule not called from the // block rule: at the root level. // primary: function () { var mixin = this.mixin, $re = _$re, root = [], node; while (current) { node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() || mixin.call() || this.comment() || this.rulesetCall() || this.directive(); if (node) { root.push(node); } else { if (!($re(/^[\s\n]+/) || $re(/^;+/))) { break; } } if (peekChar('}')) { break; } } return root; }, // We create a Comment node for CSS comments `/* */`, // but keep the LeSS comments `//` silent, by just skipping // over them. comment: function () { var comment; if (input.charAt(i) !== '/') { return; } if (input.charAt(i + 1) === '/') { return new(tree.Comment)($re(/^\/\/.*/), true, i, env.currentFileInfo); } comment = $re(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/); if (comment) { return new(tree.Comment)(comment, false, i, env.currentFileInfo); } }, comments: function () { var comment, comments = []; while(true) { comment = this.comment(); if (!comment) { break; } comments.push(comment); } return comments; }, // // Entities are tokens which can be found inside an Expression // entities: { // // A string, which supports escaping " and ' // // "milky way" 'he\'s the one!' // quoted: function () { var str, j = i, e, index = i; if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings if (input.charAt(j) !== '"' && input.charAt(j) !== "'") { return; } if (e) { $char('~'); } str = $re(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/); if (str) { return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo); } }, // // A catch-all word, such as: // // black border-collapse // keyword: function () { var k; k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/); if (k) { var color = tree.Color.fromKeyword(k); if (color) { return color; } return new(tree.Keyword)(k); } }, // // A function call // // rgb(255, 0, 255) // // We also try to catch IE's `alpha()`, but let the `alpha` parser // deal with the details. // // The arguments are parsed with the `entities.arguments` parser. // call: function () { var name, nameLC, args, alpha_ret, index = i; name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(current); if (!name) { return; } name = name[1]; nameLC = name.toLowerCase(); if (nameLC === 'url') { return null; } i += name.length; if (nameLC === 'alpha') { alpha_ret = parsers.alpha(); if(typeof alpha_ret !== 'undefined') { return alpha_ret; } } $char('('); // Parse the '(' and consume whitespace. args = this.arguments(); if (! $char(')')) { return; } if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); } }, arguments: function () { var args = [], arg; while (true) { arg = this.assignment() || parsers.expression(); if (!arg) { break; } args.push(arg); if (! $char(',')) { break; } } return args; }, literal: function () { return this.dimension() || this.color() || this.quoted() || this.unicodeDescriptor(); }, // Assignments are argument entities for calls. // They are present in ie filter properties as shown below. // // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) // assignment: function () { var key, value; key = $re(/^\w+(?=\s?=)/i); if (!key) { return; } if (!$char('=')) { return; } value = parsers.entity(); if (value) { return new(tree.Assignment)(key, value); } }, // // Parse url() tokens // // We use a specific rule for urls, because they don't really behave like // standard function calls. The difference is that the argument doesn't have // to be enclosed within a string, so it can't be parsed as an Expression. // url: function () { var value; if (input.charAt(i) !== 'u' || !$re(/^url\(/)) { return; } value = this.quoted() || this.variable() || $re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || ""; expectChar(')'); return new(tree.URL)((value.value != null || value instanceof tree.Variable) ? value : new(tree.Anonymous)(value), env.currentFileInfo); }, // // A Variable entity, such as `@fink`, in // // width: @fink + 2px // // We use a different parser for variable definitions, // see `parsers.variable`. // variable: function () { var name, index = i; if (input.charAt(i) === '@' && (name = $re(/^@@?[\w-]+/))) { return new(tree.Variable)(name, index, env.currentFileInfo); } }, // A variable entity useing the protective {} e.g. @{var} variableCurly: function () { var curly, index = i; if (input.charAt(i) === '@' && (curly = $re(/^@\{([\w-]+)\}/))) { return new(tree.Variable)("@" + curly[1], index, env.currentFileInfo); } }, // // A Hexadecimal color // // #4F3C2F // // `rgb` and `hsl` colors are parsed through the `entities.call` parser. // color: function () { var rgb; if (input.charAt(i) === '#' && (rgb = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) { return new(tree.Color)(rgb[1]); } }, // // A Dimension, that is, a number and a unit // // 0.5em 95% // dimension: function () { var value, c = input.charCodeAt(i); //Is the first char of the dimension 0-9, '.', '+' or '-' if ((c > 57 || c < 43) || c === 47 || c == 44) { return; } value = $re(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/); if (value) { return new(tree.Dimension)(value[1], value[2]); } }, // // A unicode descriptor, as is used in unicode-range // // U+0?? or U+00A1-00A9 // unicodeDescriptor: function () { var ud; ud = $re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/); if (ud) { return new(tree.UnicodeDescriptor)(ud[0]); } }, // // JavaScript code to be evaluated // // `window.location.href` // javascript: function () { var str, j = i, e; if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings if (input.charAt(j) !== '`') { return; } if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) { error("You are using JavaScript, which has been disabled."); } if (e) { $char('~'); } str = $re(/^`([^`]*)`/); if (str) { return new(tree.JavaScript)(str[1], i, e); } } }, // // The variable part of a variable definition. Used in the `rule` parser // // @fink: // variable: function () { var name; if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*:/))) { return name[1]; } }, // // The variable part of a variable definition. Used in the `rule` parser // // @fink(); // rulesetCall: function () { var name; if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) { return new tree.RulesetCall(name[1]); } }, // // extend syntax - used to extend selectors // extend: function(isRule) { var elements, e, index = i, option, extendList, extend; if (!(isRule ? $re(/^&:extend\(/) : $re(/^:extend\(/))) { return; } do { option = null; elements = null; while (! (option = $re(/^(all)(?=\s*(\)|,))/))) { e = this.element(); if (!e) { break; } if (elements) { elements.push(e); } else { elements = [ e ]; } } option = option && option[1]; extend = new(tree.Extend)(new(tree.Selector)(elements), option, index); if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; } } while($char(",")); expect(/^\)/); if (isRule) { expect(/^;/); } return extendList; }, // // extendRule - used in a rule to extend all the parent selectors // extendRule: function() { return this.extend(true); }, // // Mixins // mixin: { // // A Mixin call, with an optional argument list // // #mixins > .square(#fff); // .rounded(4px, black); // .button; // // The `while` loop is there because mixins can be // namespaced, but we only support the child and descendant // selector for now. // call: function () { var s = input.charAt(i), important = false, index = i, elemIndex, elements, elem, e, c, args; if (s !== '.' && s !== '#') { return; } save(); // stop us absorbing part of an invalid selector while (true) { elemIndex = i; e = $re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/); if (!e) { break; } elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo); if (elements) { elements.push(elem); } else { elements = [ elem ]; } c = $char('>'); } if (elements) { if ($char('(')) { args = this.args(true).args; expectChar(')'); } if (parsers.important()) { important = true; } if (parsers.end()) { forget(); return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important); } } restore(); }, args: function (isCall) { var parsers = parser.parsers, entities = parsers.entities, returner = { args:null, variadic: false }, expressions = [], argsSemiColon = [], argsComma = [], isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg; save(); while (true) { if (isCall) { arg = parsers.detachedRuleset() || parsers.expression(); } else { parsers.comments(); if (input.charAt(i) === '.' && $re(/^\.{3}/)) { returner.variadic = true; if ($char(";") && !isSemiColonSeperated) { isSemiColonSeperated = true; } (isSemiColonSeperated ? argsSemiColon : argsComma) .push({ variadic: true }); break; } arg = entities.variable() || entities.literal() || entities.keyword(); } if (!arg) { break; } nameLoop = null; if (arg.throwAwayComments) { arg.throwAwayComments(); } value = arg; var val = null; if (isCall) { // Variable if (arg.value && arg.value.length == 1) { val = arg.value[0]; } } else { val = arg; } if (val && val instanceof tree.Variable) { if ($char(':')) { if (expressions.length > 0) { if (isSemiColonSeperated) { error("Cannot mix ; and , as delimiter types"); } expressionContainsNamed = true; } // we do not support setting a ruleset as a default variable - it doesn't make sense // However if we do want to add it, there is nothing blocking it, just don't error // and remove isCall dependency below value = (isCall && parsers.detachedRuleset()) || parsers.expression(); if (!value) { if (isCall) { error("could not understand value for named argument"); } else { restore(); returner.args = []; return returner; } } nameLoop = (name = val.name); } else if (!isCall && $re(/^\.{3}/)) { returner.variadic = true; if ($char(";") && !isSemiColonSeperated) { isSemiColonSeperated = true; } (isSemiColonSeperated ? argsSemiColon : argsComma) .push({ name: arg.name, variadic: true }); break; } else if (!isCall) { name = nameLoop = val.name; value = null; } } if (value) { expressions.push(value); } argsComma.push({ name:nameLoop, value:value }); if ($char(',')) { continue; } if ($char(';') || isSemiColonSeperated) { if (expressionContainsNamed) { error("Cannot mix ; and , as delimiter types"); } isSemiColonSeperated = true; if (expressions.length > 1) { value = new(tree.Value)(expressions); } argsSemiColon.push({ name:name, value:value }); name = null; expressions = []; expressionContainsNamed = false; } } forget(); returner.args = isSemiColonSeperated ? argsSemiColon : argsComma; return returner; }, // // A Mixin definition, with a list of parameters // // .rounded (@radius: 2px, @color) { // ... // } // // Until we have a finer grained state-machine, we have to // do a look-ahead, to make sure we don't have a mixin call. // See the `rule` function for more information. // // We start by matching `.rounded (`, and then proceed on to // the argument list, which has optional default values. // We store the parameters in `params`, with a `value` key, // if there is a value, such as in the case of `@radius`. // // Once we've got our params list, and a closing `)`, we parse // the `{...}` block. // definition: function () { var name, params = [], match, ruleset, cond, variadic = false; if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || peek(/^[^{]*\}/)) { return; } save(); match = $re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/); if (match) { name = match[1]; var argInfo = this.args(false); params = argInfo.args; variadic = argInfo.variadic; // .mixincall("@{a}"); // looks a bit like a mixin definition.. // also // .mixincall(@a: {rule: set;}); // so we have to be nice and restore if (!$char(')')) { furthest = i; restore(); return; } parsers.comments(); if ($re(/^when/)) { // Guard cond = expect(parsers.conditions, 'expected condition'); } ruleset = parsers.block(); if (ruleset) { forget(); return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); } else { restore(); } } else { forget(); } } }, // // Entities are the smallest recognized token, // and can be found inside a rule's value. // entity: function () { var entities = this.entities; return entities.literal() || entities.variable() || entities.url() || entities.call() || entities.keyword() || entities.javascript() || this.comment(); }, // // A Rule terminator. Note that we use `peek()` to check for '}', // because the `block` rule will be expecting it, but we still need to make sure // it's there, if ';' was ommitted. // end: function () { return $char(';') || peekChar('}'); }, // // IE's alpha function // // alpha(opacity=88) // alpha: function () { var value; if (! $re(/^\(opacity=/i)) { return; } value = $re(/^\d+/) || this.entities.variable(); if (value) { expectChar(')'); return new(tree.Alpha)(value); } }, // // A Selector Element // // div // + h1 // #socks // input[type="text"] // // Elements are the building blocks for Selectors, // they are made out of a `Combinator` (see combinator rule), // and an element name, such as a tag a class, or `*`. // element: function () { var e, c, v, index = i; c = this.combinator(); e = $re(/^(?:\d+\.\d+|\d+)%/) || $re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) || $char('*') || $char('&') || this.attribute() || $re(/^\([^()@]+\)/) || $re(/^[\.#](?=@)/) || this.entities.variableCurly(); if (! e) { save(); if ($char('(')) { if ((v = this.selector()) && $char(')')) { e = new(tree.Paren)(v); forget(); } else { restore(); } } else { forget(); } } if (e) { return new(tree.Element)(c, e, index, env.currentFileInfo); } }, // // Combinators combine elements together, in a Selector. // // Because our parser isn't white-space sensitive, special care // has to be taken, when parsing the descendant combinator, ` `, // as it's an empty space. We have to check the previous character // in the input, to see if it's a ` ` character. More info on how // we deal with this in *combinator.js*. // combinator: function () { var c = input.charAt(i); if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') { i++; if (input.charAt(i) === '^') { c = '^^'; i++; } while (isWhitespace(input, i)) { i++; } return new(tree.Combinator)(c); } else if (isWhitespace(input, i - 1)) { return new(tree.Combinator)(" "); } else { return new(tree.Combinator)(null); } }, // // A CSS selector (see selector below) // with less extensions e.g. the ability to extend and guard // lessSelector: function () { return this.selector(true); }, // // A CSS Selector // // .class > div + h1 // li a:hover // // Selectors are made out of one or more Elements, see above. // selector: function (isLess) { var index = i, $re = _$re, elements, extendList, c, e, extend, when, condition; while ((isLess && (extend = this.extend())) || (isLess && (when = $re(/^when/))) || (e = this.element())) { if (when) { condition = expect(this.conditions, 'expected condition'); } else if (condition) { error("CSS guard can only be used at the end of selector"); } else if (extend) { if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; } } else { if (extendList) { error("Extend can only be used at the end of selector"); } c = input.charAt(i); if (elements) { elements.push(e); } else { elements = [ e ]; } e = null; } if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { break; } } if (elements) { return new(tree.Selector)(elements, extendList, condition, index, env.currentFileInfo); } if (extendList) { error("Extend must be used to extend a selector, it cannot be used on its own"); } }, attribute: function () { if (! $char('[')) { return; } var entities = this.entities, key, val, op; if (!(key = entities.variableCurly())) { key = expect(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/); } op = $re(/^[|~*$^]?=/); if (op) { val = entities.quoted() || $re(/^[0-9]+%/) || $re(/^[\w-]+/) || entities.variableCurly(); } expectChar(']'); return new(tree.Attribute)(key, op, val); }, // // The `block` rule is used by `ruleset` and `mixin.definition`. // It's a wrapper around the `primary` rule, with added `{}`. // block: function () { var content; if ($char('{') && (content = this.primary()) && $char('}')) { return content; } }, blockRuleset: function() { var block = this.block(); if (block) { block = new tree.Ruleset(null, block); } return block; }, detachedRuleset: function() { var blockRuleset = this.blockRuleset(); if (blockRuleset) { return new tree.DetachedRuleset(blockRuleset); } }, // // div, .class, body > p {...} // ruleset: function () { var selectors, s, rules, debugInfo; save(); if (env.dumpLineNumbers) { debugInfo = getDebugInfo(i, input, env); } while (true) { s = this.lessSelector(); if (!s) { break; } if (selectors) { selectors.push(s); } else { selectors = [ s ]; } this.comments(); if (s.condition && selectors.length > 1) { error("Guards are only currently allowed on a single selector."); } if (! $char(',')) { break; } if (s.condition) { error("Guards are only currently allowed on a single selector."); } this.comments(); } if (selectors && (rules = this.block())) { forget(); var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports); if (env.dumpLineNumbers) { ruleset.debugInfo = debugInfo; } return ruleset; } else { // Backtrack furthest = i; restore(); } }, rule: function (tryAnonymous) { var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable; if (c === '.' || c === '#' || c === '&') { return; } save(); name = this.variable() || this.ruleProperty(); if (name) { isVariable = typeof name === "string"; if (isVariable) { value = this.detachedRuleset(); } if (!value) { // prefer to try to parse first if its a variable or we are compressing // but always fallback on the other one value = !tryAnonymous && (env.compress || isVariable) ? (this.value() || this.anonymousValue()) : (this.anonymousValue() || this.value()); important = this.important(); // a name returned by this.ruleProperty() is always an array of the form: // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] // where each item is a tree.Keyword or tree.Variable merge = !isVariable && name.pop().value; } if (value && this.end()) { forget(); return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo); } else { furthest = i; restore(); if (value && !tryAnonymous) { return this.rule(true); } } } else { forget(); } }, anonymousValue: function () { var match; match = /^([^@+\/'"*`(;{}-]*);/.exec(current); if (match) { i += match[0].length - 1; return new(tree.Anonymous)(match[1]); } }, // // An @import directive // // @import "lib"; // // Depending on our environemnt, importing is done differently: // In the browser, it's an XHR request, in Node, it would be a // file-system operation. The function used for importing is // stored in `import`, which we pass to the Import constructor. // "import": function () { var path, features, index = i; save(); var dir = $re(/^@import?\s+/); var options = (dir ? this.importOptions() : null) || {}; if (dir && (path = this.entities.quoted() || this.entities.url())) { features = this.mediaFeatures(); if ($char(';')) { forget(); features = features && new(tree.Value)(features); return new(tree.Import)(path, features, options, index, env.currentFileInfo); } } restore(); }, importOptions: function() { var o, options = {}, optionName, value; // list of options, surrounded by parens if (! $char('(')) { return null; } do { o = this.importOption(); if (o) { optionName = o; value = true; switch(optionName) { case "css": optionName = "less"; value = false; break; case "once": optionName = "multiple"; value = false; break; } options[optionName] = value; if (! $char(',')) { break; } } } while (o); expectChar(')'); return options; }, importOption: function() { var opt = $re(/^(less|css|multiple|once|inline|reference)/); if (opt) { return opt[1]; } }, mediaFeature: function () { var entities = this.entities, nodes = [], e, p; do { e = entities.keyword() || entities.variable(); if (e) { nodes.push(e); } else if ($char('(')) { p = this.property(); e = this.value(); if ($char(')')) { if (p && e) { nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true))); } else if (e) { nodes.push(new(tree.Paren)(e)); } else { return null; } } else { return null; } } } while (e); if (nodes.length > 0) { return new(tree.Expression)(nodes); } }, mediaFeatures: function () { var entities = this.entities, features = [], e; do { e = this.mediaFeature(); if (e) { features.push(e); if (! $char(',')) { break; } } else { e = entities.variable(); if (e) { features.push(e); if (! $char(',')) { break; } } } } while (e); return features.length > 0 ? features : null; }, media: function () { var features, rules, media, debugInfo; if (env.dumpLineNumbers) { debugInfo = getDebugInfo(i, input, env); } if ($re(/^@media/)) { features = this.mediaFeatures(); rules = this.block(); if (rules) { media = new(tree.Media)(rules, features, i, env.currentFileInfo); if (env.dumpLineNumbers) { media.debugInfo = debugInfo; } return media; } } }, // // A CSS Directive // // @charset "utf-8"; // directive: function () { var index = i, name, value, rules, nonVendorSpecificName, hasIdentifier, hasExpression, hasUnknown, hasBlock = true; if (input.charAt(i) !== '@') { return; } value = this['import']() || this.media(); if (value) { return value; } save(); name = $re(/^@[a-z-]+/); if (!name) { return; } nonVendorSpecificName = name; if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) { nonVendorSpecificName = "@" + name.slice(name.indexOf('-', 2) + 1); } switch(nonVendorSpecificName) { /* case "@font-face": case "@viewport": case "@top-left": case "@top-left-corner": case "@top-center": case "@top-right": case "@top-right-corner": case "@bottom-left": case "@bottom-left-corner": case "@bottom-center": case "@bottom-right": case "@bottom-right-corner": case "@left-top": case "@left-middle": case "@left-bottom": case "@right-top": case "@right-middle": case "@right-bottom": hasBlock = true; break; */ case "@charset": hasIdentifier = true; hasBlock = false; break; case "@namespace": hasExpression = true; hasBlock = false; break; case "@keyframes": hasIdentifier = true; break; case "@host": case "@page": case "@document": case "@supports": hasUnknown = true; break; } if (hasIdentifier) { value = this.entity(); if (!value) { error("expected " + name + " identifier"); } } else if (hasExpression) { value = this.expression(); if (!value) { error("expected " + name + " expression"); } } else if (hasUnknown) { value = ($re(/^[^{;]+/) || '').trim(); if (value) { value = new(tree.Anonymous)(value); } } if (hasBlock) { rules = this.blockRuleset(); } if (rules || (!hasBlock && value && $char(';'))) { forget(); return new(tree.Directive)(name, value, rules, index, env.currentFileInfo, env.dumpLineNumbers ? getDebugInfo(index, input, env) : null); } restore(); }, // // A Value is a comma-delimited list of Expressions // // font-family: Baskerville, Georgia, serif; // // In a Rule, a Value represents everything after the `:`, // and before the `;`. // value: function () { var e, expressions = []; do { e = this.expression(); if (e) { expressions.push(e); if (! $char(',')) { break; } } } while(e); if (expressions.length > 0) { return new(tree.Value)(expressions); } }, important: function () { if (input.charAt(i) === '!') { return $re(/^! *important/); } }, sub: function () { var a, e; if ($char('(')) { a = this.addition(); if (a) { e = new(tree.Expression)([a]); expectChar(')'); e.parens = true; return e; } } }, multiplication: function () { var m, a, op, operation, isSpaced; m = this.operand(); if (m) { isSpaced = isWhitespace(input, i - 1); while (true) { if (peek(/^\/[*\/]/)) { break; } op = $char('/') || $char('*'); if (!op) { break; } a = this.operand(); if (!a) { break; } m.parensInOp = true; a.parensInOp = true; operation = new(tree.Operation)(op, [operation || m, a], isSpaced); isSpaced = isWhitespace(input, i - 1); } return operation || m; } }, addition: function () { var m, a, op, operation, isSpaced; m = this.multiplication(); if (m) { isSpaced = isWhitespace(input, i - 1); while (true) { op = $re(/^[-+]\s+/) || (!isSpaced && ($char('+') || $char('-'))); if (!op) { break; } a = this.multiplication(); if (!a) { break; } m.parensInOp = true; a.parensInOp = true; operation = new(tree.Operation)(op, [operation || m, a], isSpaced); isSpaced = isWhitespace(input, i - 1); } return operation || m; } }, conditions: function () { var a, b, index = i, condition; a = this.condition(); if (a) { while (true) { if (!peek(/^,\s*(not\s*)?\(/) || !$char(',')) { break; } b = this.condition(); if (!b) { break; } condition = new(tree.Condition)('or', condition || a, b, index); } return condition || a; } }, condition: function () { var entities = this.entities, index = i, negate = false, a, b, c, op; if ($re(/^not/)) { negate = true; } expectChar('('); a = this.addition() || entities.keyword() || entities.quoted(); if (a) { op = $re(/^(?:>=|<=|=<|[<=>])/); if (op) { b = this.addition() || entities.keyword() || entities.quoted(); if (b) { c = new(tree.Condition)(op, a, b, index, negate); } else { error('expected expression'); } } else { c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); } expectChar(')'); return $re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c; } }, // // An operand is anything that can be part of an operation, // such as a Color, or a Variable // operand: function () { var entities = this.entities, p = input.charAt(i + 1), negate; if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $char('-'); } var o = this.sub() || entities.dimension() || entities.color() || entities.variable() || entities.call(); if (negate) { o.parensInOp = true; o = new(tree.Negative)(o); } return o; }, // // Expressions either represent mathematical operations, // or white-space delimited Entities. // // 1px solid black // @var * 2 // expression: function () { var entities = [], e, delim; do { e = this.addition() || this.entity(); if (e) { entities.push(e); // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here if (!peek(/^\/[\/*]/)) { delim = $char('/'); if (delim) { entities.push(new(tree.Anonymous)(delim)); } } } } while (e); if (entities.length > 0) { return new(tree.Expression)(entities); } }, property: function () { var name = $re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/); if (name) { return name[1]; } }, ruleProperty: function () { var c = current, name = [], index = [], length = 0, s, k; function match(re) { var a = re.exec(c); if (a) { index.push(i + length); length += a[0].length; c = c.slice(a[1].length); return name.push(a[1]); } } match(/^(\*?)/); while (match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)); // ! if ((name.length > 1) && match(/^\s*((?:\+_|\+)?)\s*:/)) { // at last, we have the complete match now. move forward, // convert name particles to tree objects and return: skipWhitespace(length); if (name[0] === '') { name.shift(); index.shift(); } for (k = 0; k < name.length; k++) { s = name[k]; name[k] = (s.charAt(0) !== '@') ? new(tree.Keyword)(s) : new(tree.Variable)('@' + s.slice(2, -1), index[k], env.currentFileInfo); } return name; } } } }; return parser; }; less.Parser.serializeVars = function(vars) { var s = ''; for (var name in vars) { if (Object.hasOwnProperty.call(vars, name)) { var value = vars[name]; s += ((name[0] === '@') ? '' : '@') + name +': '+ value + ((('' + value).slice(-1) === ';') ? '' : ';'); } } return s; }; (function (tree) { tree.functions = { rgb: function (r, g, b) { return this.rgba(r, g, b, 1.0); }, rgba: function (r, g, b, a) { var rgb = [r, g, b].map(function (c) { return scaled(c, 255); }); a = number(a); return new(tree.Color)(rgb, a); }, hsl: function (h, s, l) { return this.hsla(h, s, l, 1.0); }, hsla: function (h, s, l, a) { function hue(h) { h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } else if (h * 2 < 1) { return m2; } else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; } else { return m1; } } h = (number(h) % 360) / 360; s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a)); var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; var m1 = l * 2 - m2; return this.rgba(hue(h + 1/3) * 255, hue(h) * 255, hue(h - 1/3) * 255, a); }, hsv: function(h, s, v) { return this.hsva(h, s, v, 1.0); }, hsva: function(h, s, v, a) { h = ((number(h) % 360) / 360) * 360; s = number(s); v = number(v); a = number(a); var i, f; i = Math.floor((h / 60) % 6); f = (h / 60) - i; var vs = [v, v * (1 - s), v * (1 - f * s), v * (1 - (1 - f) * s)]; var perm = [[0, 3, 1], [2, 0, 1], [1, 0, 3], [1, 2, 0], [3, 1, 0], [0, 1, 2]]; return this.rgba(vs[perm[i][0]] * 255, vs[perm[i][1]] * 255, vs[perm[i][2]] * 255, a); }, hue: function (color) { return new(tree.Dimension)(Math.round(color.toHSL().h)); }, saturation: function (color) { return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); }, lightness: function (color) { return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); }, hsvhue: function(color) { return new(tree.Dimension)(Math.round(color.toHSV().h)); }, hsvsaturation: function (color) { return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%'); }, hsvvalue: function (color) { return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%'); }, red: function (color) { return new(tree.Dimension)(color.rgb[0]); }, green: function (color) { return new(tree.Dimension)(color.rgb[1]); }, blue: function (color) { return new(tree.Dimension)(color.rgb[2]); }, alpha: function (color) { return new(tree.Dimension)(color.toHSL().a); }, luma: function (color) { return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%'); }, luminance: function (color) { var luminance = (0.2126 * color.rgb[0] / 255) + (0.7152 * color.rgb[1] / 255) + (0.0722 * color.rgb[2] / 255); return new(tree.Dimension)(Math.round(luminance * color.alpha * 100), '%'); }, saturate: function (color, amount) { // filter: saturate(3.2); // should be kept as is, so check for color if (!color.rgb) { return null; } var hsl = color.toHSL(); hsl.s += amount.value / 100; hsl.s = clamp(hsl.s); return hsla(hsl); }, desaturate: function (color, amount) { var hsl = color.toHSL(); hsl.s -= amount.value / 100; hsl.s = clamp(hsl.s); return hsla(hsl); }, lighten: function (color, amount) { var hsl = color.toHSL(); hsl.l += amount.value / 100; hsl.l = clamp(hsl.l); return hsla(hsl); }, darken: function (color, amount) { var hsl = color.toHSL(); hsl.l -= amount.value / 100; hsl.l = clamp(hsl.l); return hsla(hsl); }, fadein: function (color, amount) { var hsl = color.toHSL(); hsl.a += amount.value / 100; hsl.a = clamp(hsl.a); return hsla(hsl); }, fadeout: function (color, amount) { var hsl = color.toHSL(); hsl.a -= amount.value / 100; hsl.a = clamp(hsl.a); return hsla(hsl); }, fade: function (color, amount) { var hsl = color.toHSL(); hsl.a = amount.value / 100; hsl.a = clamp(hsl.a); return hsla(hsl); }, spin: function (color, amount) { var hsl = color.toHSL(); var hue = (hsl.h + amount.value) % 360; hsl.h = hue < 0 ? 360 + hue : hue; return hsla(hsl); }, // // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein // http://sass-lang.com // mix: function (color1, color2, weight) { if (!weight) { weight = new(tree.Dimension)(50); } var p = weight.value / 100.0; var w = p * 2 - 1; var a = color1.toHSL().a - color2.toHSL().a; var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; var w2 = 1 - w1; var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, color1.rgb[1] * w1 + color2.rgb[1] * w2, color1.rgb[2] * w1 + color2.rgb[2] * w2]; var alpha = color1.alpha * p + color2.alpha * (1 - p); return new(tree.Color)(rgb, alpha); }, greyscale: function (color) { return this.desaturate(color, new(tree.Dimension)(100)); }, contrast: function (color, dark, light, threshold) { // filter: contrast(3.2); // should be kept as is, so check for color if (!color.rgb) { return null; } if (typeof light === 'undefined') { light = this.rgba(255, 255, 255, 1.0); } if (typeof dark === 'undefined') { dark = this.rgba(0, 0, 0, 1.0); } //Figure out which is actually light and dark! if (dark.luma() > light.luma()) { var t = light; light = dark; dark = t; } if (typeof threshold === 'undefined') { threshold = 0.43; } else { threshold = number(threshold); } if (color.luma() < threshold) { return light; } else { return dark; } }, e: function (str) { return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); }, escape: function (str) { return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); }, replace: function (string, pattern, replacement, flags) { var result = string.value; result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value); return new(tree.Quoted)(string.quote || '', result, string.escaped); }, '%': function (string /* arg, arg, ...*/) { var args = Array.prototype.slice.call(arguments, 1), result = string.value; for (var i = 0; i < args.length; i++) { /*jshint loopfunc:true */ result = result.replace(/%[sda]/i, function(token) { var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; }); } result = result.replace(/%%/g, '%'); return new(tree.Quoted)(string.quote || '', result, string.escaped); }, unit: function (val, unit) { if(!(val instanceof tree.Dimension)) { throw { type: "Argument", message: "the first argument to unit must be a number" + (val instanceof tree.Operation ? ". Have you forgotten parenthesis?" : "") }; } if (unit) { if (unit instanceof tree.Keyword) { unit = unit.value; } else { unit = unit.toCSS(); } } else { unit = ""; } return new(tree.Dimension)(val.value, unit); }, convert: function (val, unit) { return val.convertTo(unit.value); }, round: function (n, f) { var fraction = typeof(f) === "undefined" ? 0 : f.value; return _math(function(num) { return num.toFixed(fraction); }, null, n); }, pi: function () { return new(tree.Dimension)(Math.PI); }, mod: function(a, b) { return new(tree.Dimension)(a.value % b.value, a.unit); }, pow: function(x, y) { if (typeof x === "number" && typeof y === "number") { x = new(tree.Dimension)(x); y = new(tree.Dimension)(y); } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) { throw { type: "Argument", message: "arguments must be numbers" }; } return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit); }, _minmax: function (isMin, args) { args = Array.prototype.slice.call(args); switch(args.length) { case 0: throw { type: "Argument", message: "one or more arguments required" }; } var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone, order = [], // elems only contains original argument values. values = {}; // key is the unit.toString() for unified tree.Dimension values, // value is the index into the order array. for (i = 0; i < args.length; i++) { current = args[i]; if (!(current instanceof tree.Dimension)) { if(Array.isArray(args[i].value)) { Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value)); } continue; } currentUnified = current.unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify(); unit = currentUnified.unit.toString() === "" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString(); unitStatic = unit !== "" && unitStatic === undefined || unit !== "" && order[0].unify().unit.toString() === "" ? unit : unitStatic; unitClone = unit !== "" && unitClone === undefined ? current.unit.toString() : unitClone; j = values[""] !== undefined && unit !== "" && unit === unitStatic ? values[""] : values[unit]; if (j === undefined) { if(unitStatic !== undefined && unit !== unitStatic) { throw{ type: "Argument", message: "incompatible types" }; } values[unit] = order.length; order.push(current); continue; } referenceUnified = order[j].unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify(); if ( isMin && currentUnified.value < referenceUnified.value || !isMin && currentUnified.value > referenceUnified.value) { order[j] = current; } } if (order.length == 1) { return order[0]; } args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? "," : ", "); return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); }, min: function () { return this._minmax(true, arguments); }, max: function () { return this._minmax(false, arguments); }, "get-unit": function (n) { return new(tree.Anonymous)(n.unit); }, argb: function (color) { return new(tree.Anonymous)(color.toARGB()); }, percentage: function (n) { return new(tree.Dimension)(n.value * 100, '%'); }, color: function (n) { if (n instanceof tree.Quoted) { var colorCandidate = n.value, returnColor; returnColor = tree.Color.fromKeyword(colorCandidate); if (returnColor) { return returnColor; } if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/.test(colorCandidate)) { return new(tree.Color)(colorCandidate.slice(1)); } throw { type: "Argument", message: "argument must be a color keyword or 3/6 digit hex e.g. #FFF" }; } else { throw { type: "Argument", message: "argument must be a string" }; } }, iscolor: function (n) { return this._isa(n, tree.Color); }, isnumber: function (n) { return this._isa(n, tree.Dimension); }, isstring: function (n) { return this._isa(n, tree.Quoted); }, iskeyword: function (n) { return this._isa(n, tree.Keyword); }, isurl: function (n) { return this._isa(n, tree.URL); }, ispixel: function (n) { return this.isunit(n, 'px'); }, ispercentage: function (n) { return this.isunit(n, '%'); }, isem: function (n) { return this.isunit(n, 'em'); }, isunit: function (n, unit) { return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False; }, _isa: function (n, Type) { return (n instanceof Type) ? tree.True : tree.False; }, tint: function(color, amount) { return this.mix(this.rgb(255,255,255), color, amount); }, shade: function(color, amount) { return this.mix(this.rgb(0, 0, 0), color, amount); }, extract: function(values, index) { index = index.value - 1; // (1-based index) // handle non-array values as an array of length 1 // return 'undefined' if index is invalid return Array.isArray(values.value) ? values.value[index] : Array(values)[index]; }, length: function(values) { var n = Array.isArray(values.value) ? values.value.length : 1; return new tree.Dimension(n); }, "data-uri": function(mimetypeNode, filePathNode) { if (typeof window !== 'undefined') { return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); } var mimetype = mimetypeNode.value; var filePath = (filePathNode && filePathNode.value); var fs = require('fs'), path = require('path'), useBase64 = false; if (arguments.length < 2) { filePath = mimetype; } if (this.env.isPathRelative(filePath)) { if (this.currentFileInfo.relativeUrls) { filePath = path.join(this.currentFileInfo.currentDirectory, filePath); } else { filePath = path.join(this.currentFileInfo.entryPath, filePath); } } // detect the mimetype if not given if (arguments.length < 2) { var mime; try { mime = require('mime'); } catch (ex) { mime = tree._mime; } mimetype = mime.lookup(filePath); // use base 64 unless it's an ASCII or UTF-8 format var charset = mime.charsets.lookup(mimetype); useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; if (useBase64) { mimetype += ';base64'; } } else { useBase64 = /;base64$/.test(mimetype); } var buf = fs.readFileSync(filePath); // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded // and the --ieCompat flag is enabled, return a normal url() instead. var DATA_URI_MAX_KB = 32, fileSizeInKB = parseInt((buf.length / 1024), 10); if (fileSizeInKB >= DATA_URI_MAX_KB) { if (this.env.ieCompat !== false) { if (!this.env.silent) { console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); } return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); } } buf = useBase64 ? buf.toString('base64') : encodeURIComponent(buf); var uri = "\"data:" + mimetype + ',' + buf + "\""; return new(tree.URL)(new(tree.Anonymous)(uri)); }, "svg-gradient": function(direction) { function throwArgumentDescriptor() { throw { type: "Argument", message: "svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]" }; } if (arguments.length < 3) { throwArgumentDescriptor(); } var stops = Array.prototype.slice.call(arguments, 1), gradientDirectionSvg, gradientType = "linear", rectangleDimension = 'x="0" y="0" width="1" height="1"', useBase64 = true, renderEnv = {compress: false}, returner, directionValue = direction.toCSS(renderEnv), i, color, position, positionValue, alpha; switch (directionValue) { case "to bottom": gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; break; case "to right": gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; break; case "to bottom right": gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; break; case "to top right": gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; break; case "ellipse": case "ellipse at center": gradientType = "radial"; gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; break; default: throw { type: "Argument", message: "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" }; } returner = '' + '' + '<' + gradientType + 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' + gradientDirectionSvg + '>'; for (i = 0; i < stops.length; i+= 1) { if (stops[i].value) { color = stops[i].value[0]; position = stops[i].value[1]; } else { color = stops[i]; position = undefined; } if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) { throwArgumentDescriptor(); } positionValue = position ? position.toCSS(renderEnv) : i === 0 ? "0%" : "100%"; alpha = color.alpha; returner += ''; } returner += '' + ''; if (useBase64) { try { returner = require('./encoder').encodeBase64(returner); // TODO browser implementation } catch(e) { useBase64 = false; } } returner = "'data:image/svg+xml" + (useBase64 ? ";base64" : "") + "," + returner + "'"; return new(tree.URL)(new(tree.Anonymous)(returner)); } }; // these static methods are used as a fallback when the optional 'mime' dependency is missing tree._mime = { // this map is intentionally incomplete // if you want more, install 'mime' dep _types: { '.htm' : 'text/html', '.html': 'text/html', '.gif' : 'image/gif', '.jpg' : 'image/jpeg', '.jpeg': 'image/jpeg', '.png' : 'image/png' }, lookup: function (filepath) { var ext = require('path').extname(filepath), type = tree._mime._types[ext]; if (type === undefined) { throw new Error('Optional dependency "mime" is required for ' + ext); } return type; }, charsets: { lookup: function (type) { // assumes all text types are UTF-8 return type && (/^text\//).test(type) ? 'UTF-8' : ''; } } }; // Math var mathFunctions = { // name, unit ceil: null, floor: null, sqrt: null, abs: null, tan: "", sin: "", cos: "", atan: "rad", asin: "rad", acos: "rad" }; function _math(fn, unit, n) { if (!(n instanceof tree.Dimension)) { throw { type: "Argument", message: "argument must be a number" }; } if (unit == null) { unit = n.unit; } else { n = n.unify(); } return new(tree.Dimension)(fn(parseFloat(n.value)), unit); } // ~ End of Math // Color Blending // ref: http://www.w3.org/TR/compositing-1 function colorBlend(mode, color1, color2) { var ab = color1.alpha, cb, // backdrop as = color2.alpha, cs, // source ar, cr, r = []; // result ar = as + ab * (1 - as); for (var i = 0; i < 3; i++) { cb = color1.rgb[i] / 255; cs = color2.rgb[i] / 255; cr = mode(cb, cs); if (ar) { cr = (as * cs + ab * (cb - as * (cb + cs - cr))) / ar; } r[i] = cr * 255; } return new(tree.Color)(r, ar); } var colorBlendMode = { multiply: function(cb, cs) { return cb * cs; }, screen: function(cb, cs) { return cb + cs - cb * cs; }, overlay: function(cb, cs) { cb *= 2; return (cb <= 1) ? colorBlendMode.multiply(cb, cs) : colorBlendMode.screen(cb - 1, cs); }, softlight: function(cb, cs) { var d = 1, e = cb; if (cs > 0.5) { e = 1; d = (cb > 0.25) ? Math.sqrt(cb) : ((16 * cb - 12) * cb + 4) * cb; } return cb - (1 - 2 * cs) * e * (d - cb); }, hardlight: function(cb, cs) { return colorBlendMode.overlay(cs, cb); }, difference: function(cb, cs) { return Math.abs(cb - cs); }, exclusion: function(cb, cs) { return cb + cs - 2 * cb * cs; }, // non-w3c functions: average: function(cb, cs) { return (cb + cs) / 2; }, negation: function(cb, cs) { return 1 - Math.abs(cb + cs - 1); } }; // ~ End of Color Blending tree.defaultFunc = { eval: function () { var v = this.value_, e = this.error_; if (e) { throw e; } if (v != null) { return v ? tree.True : tree.False; } }, value: function (v) { this.value_ = v; }, error: function (e) { this.error_ = e; }, reset: function () { this.value_ = this.error_ = null; } }; function initFunctions() { var f, tf = tree.functions; // math for (f in mathFunctions) { if (mathFunctions.hasOwnProperty(f)) { tf[f] = _math.bind(null, Math[f], mathFunctions[f]); } } // color blending for (f in colorBlendMode) { if (colorBlendMode.hasOwnProperty(f)) { tf[f] = colorBlend.bind(null, colorBlendMode[f]); } } // default f = tree.defaultFunc; tf["default"] = f.eval.bind(f); } initFunctions(); function hsla(color) { return tree.functions.hsla(color.h, color.s, color.l, color.a); } function scaled(n, size) { if (n instanceof tree.Dimension && n.unit.is('%')) { return parseFloat(n.value * size / 100); } else { return number(n); } } function number(n) { if (n instanceof tree.Dimension) { return parseFloat(n.unit.is('%') ? n.value / 100 : n.value); } else if (typeof(n) === 'number') { return n; } else { throw { error: "RuntimeError", message: "color functions take numbers as parameters" }; } } function clamp(val) { return Math.min(1, Math.max(0, val)); } tree.fround = function(env, value) { var p; if (env && (env.numPrecision != null)) { p = Math.pow(10, env.numPrecision); return Math.round(value * p) / p; } else { return value; } }; tree.functionCall = function(env, currentFileInfo) { this.env = env; this.currentFileInfo = currentFileInfo; }; tree.functionCall.prototype = tree.functions; })(require('./tree')); (function (tree) { tree.colors = { 'aliceblue':'#f0f8ff', 'antiquewhite':'#faebd7', 'aqua':'#00ffff', 'aquamarine':'#7fffd4', 'azure':'#f0ffff', 'beige':'#f5f5dc', 'bisque':'#ffe4c4', 'black':'#000000', 'blanchedalmond':'#ffebcd', 'blue':'#0000ff', 'blueviolet':'#8a2be2', 'brown':'#a52a2a', 'burlywood':'#deb887', 'cadetblue':'#5f9ea0', 'chartreuse':'#7fff00', 'chocolate':'#d2691e', 'coral':'#ff7f50', 'cornflowerblue':'#6495ed', 'cornsilk':'#fff8dc', 'crimson':'#dc143c', 'cyan':'#00ffff', 'darkblue':'#00008b', 'darkcyan':'#008b8b', 'darkgoldenrod':'#b8860b', 'darkgray':'#a9a9a9', 'darkgrey':'#a9a9a9', 'darkgreen':'#006400', 'darkkhaki':'#bdb76b', 'darkmagenta':'#8b008b', 'darkolivegreen':'#556b2f', 'darkorange':'#ff8c00', 'darkorchid':'#9932cc', 'darkred':'#8b0000', 'darksalmon':'#e9967a', 'darkseagreen':'#8fbc8f', 'darkslateblue':'#483d8b', 'darkslategray':'#2f4f4f', 'darkslategrey':'#2f4f4f', 'darkturquoise':'#00ced1', 'darkviolet':'#9400d3', 'deeppink':'#ff1493', 'deepskyblue':'#00bfff', 'dimgray':'#696969', 'dimgrey':'#696969', 'dodgerblue':'#1e90ff', 'firebrick':'#b22222', 'floralwhite':'#fffaf0', 'forestgreen':'#228b22', 'fuchsia':'#ff00ff', 'gainsboro':'#dcdcdc', 'ghostwhite':'#f8f8ff', 'gold':'#ffd700', 'goldenrod':'#daa520', 'gray':'#808080', 'grey':'#808080', 'green':'#008000', 'greenyellow':'#adff2f', 'honeydew':'#f0fff0', 'hotpink':'#ff69b4', 'indianred':'#cd5c5c', 'indigo':'#4b0082', 'ivory':'#fffff0', 'khaki':'#f0e68c', 'lavender':'#e6e6fa', 'lavenderblush':'#fff0f5', 'lawngreen':'#7cfc00', 'lemonchiffon':'#fffacd', 'lightblue':'#add8e6', 'lightcoral':'#f08080', 'lightcyan':'#e0ffff', 'lightgoldenrodyellow':'#fafad2', 'lightgray':'#d3d3d3', 'lightgrey':'#d3d3d3', 'lightgreen':'#90ee90', 'lightpink':'#ffb6c1', 'lightsalmon':'#ffa07a', 'lightseagreen':'#20b2aa', 'lightskyblue':'#87cefa', 'lightslategray':'#778899', 'lightslategrey':'#778899', 'lightsteelblue':'#b0c4de', 'lightyellow':'#ffffe0', 'lime':'#00ff00', 'limegreen':'#32cd32', 'linen':'#faf0e6', 'magenta':'#ff00ff', 'maroon':'#800000', 'mediumaquamarine':'#66cdaa', 'mediumblue':'#0000cd', 'mediumorchid':'#ba55d3', 'mediumpurple':'#9370d8', 'mediumseagreen':'#3cb371', 'mediumslateblue':'#7b68ee', 'mediumspringgreen':'#00fa9a', 'mediumturquoise':'#48d1cc', 'mediumvioletred':'#c71585', 'midnightblue':'#191970', 'mintcream':'#f5fffa', 'mistyrose':'#ffe4e1', 'moccasin':'#ffe4b5', 'navajowhite':'#ffdead', 'navy':'#000080', 'oldlace':'#fdf5e6', 'olive':'#808000', 'olivedrab':'#6b8e23', 'orange':'#ffa500', 'orangered':'#ff4500', 'orchid':'#da70d6', 'palegoldenrod':'#eee8aa', 'palegreen':'#98fb98', 'paleturquoise':'#afeeee', 'palevioletred':'#d87093', 'papayawhip':'#ffefd5', 'peachpuff':'#ffdab9', 'peru':'#cd853f', 'pink':'#ffc0cb', 'plum':'#dda0dd', 'powderblue':'#b0e0e6', 'purple':'#800080', 'red':'#ff0000', 'rosybrown':'#bc8f8f', 'royalblue':'#4169e1', 'saddlebrown':'#8b4513', 'salmon':'#fa8072', 'sandybrown':'#f4a460', 'seagreen':'#2e8b57', 'seashell':'#fff5ee', 'sienna':'#a0522d', 'silver':'#c0c0c0', 'skyblue':'#87ceeb', 'slateblue':'#6a5acd', 'slategray':'#708090', 'slategrey':'#708090', 'snow':'#fffafa', 'springgreen':'#00ff7f', 'steelblue':'#4682b4', 'tan':'#d2b48c', 'teal':'#008080', 'thistle':'#d8bfd8', 'tomato':'#ff6347', 'turquoise':'#40e0d0', 'violet':'#ee82ee', 'wheat':'#f5deb3', 'white':'#ffffff', 'whitesmoke':'#f5f5f5', 'yellow':'#ffff00', 'yellowgreen':'#9acd32' }; })(require('./tree')); (function (tree) { tree.debugInfo = function(env, ctx, lineSeperator) { var result=""; if (env.dumpLineNumbers && !env.compress) { switch(env.dumpLineNumbers) { case 'comments': result = tree.debugInfo.asComment(ctx); break; case 'mediaquery': result = tree.debugInfo.asMediaQuery(ctx); break; case 'all': result = tree.debugInfo.asComment(ctx) + (lineSeperator || "") + tree.debugInfo.asMediaQuery(ctx); break; } } return result; }; tree.debugInfo.asComment = function(ctx) { return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\n'; }; tree.debugInfo.asMediaQuery = function(ctx) { return '@media -sass-debug-info{filename{font-family:' + ('file://' + ctx.debugInfo.fileName).replace(/([.:\/\\])/g, function (a) { if (a == '\\') { a = '\/'; } return '\\' + a; }) + '}line{font-family:\\00003' + ctx.debugInfo.lineNumber + '}}\n'; }; tree.find = function (obj, fun) { for (var i = 0, r; i < obj.length; i++) { r = fun.call(obj, obj[i]); if (r) { return r; } } return null; }; tree.jsify = function (obj) { if (Array.isArray(obj.value) && (obj.value.length > 1)) { return '[' + obj.value.map(function (v) { return v.toCSS(false); }).join(', ') + ']'; } else { return obj.toCSS(false); } }; tree.toCSS = function (env) { var strs = []; this.genCSS(env, { add: function(chunk, fileInfo, index) { strs.push(chunk); }, isEmpty: function () { return strs.length === 0; } }); return strs.join(''); }; tree.outputRuleset = function (env, output, rules) { var ruleCnt = rules.length, i; env.tabLevel = (env.tabLevel | 0) + 1; // Compressed if (env.compress) { output.add('{'); for (i = 0; i < ruleCnt; i++) { rules[i].genCSS(env, output); } output.add('}'); env.tabLevel--; return; } // Non-compressed var tabSetStr = '\n' + Array(env.tabLevel).join(" "), tabRuleStr = tabSetStr + " "; if (!ruleCnt) { output.add(" {" + tabSetStr + '}'); } else { output.add(" {" + tabRuleStr); rules[0].genCSS(env, output); for (i = 1; i < ruleCnt; i++) { output.add(tabRuleStr); rules[i].genCSS(env, output); } output.add(tabSetStr + '}'); } env.tabLevel--; }; })(require('./tree')); (function (tree) { tree.Alpha = function (val) { this.value = val; }; tree.Alpha.prototype = { type: "Alpha", accept: function (visitor) { this.value = visitor.visit(this.value); }, eval: function (env) { if (this.value.eval) { return new tree.Alpha(this.value.eval(env)); } return this; }, genCSS: function (env, output) { output.add("alpha(opacity="); if (this.value.genCSS) { this.value.genCSS(env, output); } else { output.add(this.value); } output.add(")"); }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { tree.Anonymous = function (string, index, currentFileInfo, mapLines) { this.value = string.value || string; this.index = index; this.mapLines = mapLines; this.currentFileInfo = currentFileInfo; }; tree.Anonymous.prototype = { type: "Anonymous", eval: function () { return new tree.Anonymous(this.value, this.index, this.currentFileInfo, this.mapLines); }, compare: function (x) { if (!x.toCSS) { return -1; } var left = this.toCSS(), right = x.toCSS(); if (left === right) { return 0; } return left < right ? -1 : 1; }, genCSS: function (env, output) { output.add(this.value, this.currentFileInfo, this.index, this.mapLines); }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { tree.Assignment = function (key, val) { this.key = key; this.value = val; }; tree.Assignment.prototype = { type: "Assignment", accept: function (visitor) { this.value = visitor.visit(this.value); }, eval: function (env) { if (this.value.eval) { return new(tree.Assignment)(this.key, this.value.eval(env)); } return this; }, genCSS: function (env, output) { output.add(this.key + '='); if (this.value.genCSS) { this.value.genCSS(env, output); } else { output.add(this.value); } }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { // // A function call node. // tree.Call = function (name, args, index, currentFileInfo) { this.name = name; this.args = args; this.index = index; this.currentFileInfo = currentFileInfo; }; tree.Call.prototype = { type: "Call", accept: function (visitor) { if (this.args) { this.args = visitor.visitArray(this.args); } }, // // When evaluating a function call, // we either find the function in `tree.functions` [1], // in which case we call it, passing the evaluated arguments, // if this returns null or we cannot find the function, we // simply print it out as it appeared originally [2]. // // The *functions.js* file contains the built-in functions. // // The reason why we evaluate the arguments, is in the case where // we try to pass a variable to a function, like: `saturate(@color)`. // The function should receive the value, not the variable. // eval: function (env) { var args = this.args.map(function (a) { return a.eval(env); }), nameLC = this.name.toLowerCase(), result, func; if (nameLC in tree.functions) { // 1. try { func = new tree.functionCall(env, this.currentFileInfo); result = func[nameLC].apply(func, args); if (result != null) { return result; } } catch (e) { throw { type: e.type || "Runtime", message: "error evaluating function `" + this.name + "`" + (e.message ? ': ' + e.message : ''), index: this.index, filename: this.currentFileInfo.filename }; } } return new tree.Call(this.name, args, this.index, this.currentFileInfo); }, genCSS: function (env, output) { output.add(this.name + "(", this.currentFileInfo, this.index); for(var i = 0; i < this.args.length; i++) { this.args[i].genCSS(env, output); if (i + 1 < this.args.length) { output.add(", "); } } output.add(")"); }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { // // RGB Colors - #ff0014, #eee // tree.Color = function (rgb, a) { // // The end goal here, is to parse the arguments // into an integer triplet, such as `128, 255, 0` // // This facilitates operations and conversions. // if (Array.isArray(rgb)) { this.rgb = rgb; } else if (rgb.length == 6) { this.rgb = rgb.match(/.{2}/g).map(function (c) { return parseInt(c, 16); }); } else { this.rgb = rgb.split('').map(function (c) { return parseInt(c + c, 16); }); } this.alpha = typeof(a) === 'number' ? a : 1; }; var transparentKeyword = "transparent"; tree.Color.prototype = { type: "Color", eval: function () { return this; }, luma: function () { var r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255; r = (r <= 0.03928) ? r / 12.92 : Math.pow(((r + 0.055) / 1.055), 2.4); g = (g <= 0.03928) ? g / 12.92 : Math.pow(((g + 0.055) / 1.055), 2.4); b = (b <= 0.03928) ? b / 12.92 : Math.pow(((b + 0.055) / 1.055), 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; }, genCSS: function (env, output) { output.add(this.toCSS(env)); }, toCSS: function (env, doNotCompress) { var compress = env && env.compress && !doNotCompress, alpha = tree.fround(env, this.alpha); // If we have some transparency, the only way to represent it // is via `rgba`. Otherwise, we use the hex representation, // which has better compatibility with older browsers. // Values are capped between `0` and `255`, rounded and zero-padded. if (alpha < 1) { if (alpha === 0 && this.isTransparentKeyword) { return transparentKeyword; } return "rgba(" + this.rgb.map(function (c) { return clamp(Math.round(c), 255); }).concat(clamp(alpha, 1)) .join(',' + (compress ? '' : ' ')) + ")"; } else { var color = this.toRGB(); if (compress) { var splitcolor = color.split(''); // Convert color to short format if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) { color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5]; } } return color; } }, // // Operations have to be done per-channel, if not, // channels will spill onto each other. Once we have // our result, in the form of an integer triplet, // we create a new Color node to hold the result. // operate: function (env, op, other) { var rgb = []; var alpha = this.alpha * (1 - other.alpha) + other.alpha; for (var c = 0; c < 3; c++) { rgb[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]); } return new(tree.Color)(rgb, alpha); }, toRGB: function () { return toHex(this.rgb); }, toHSL: function () { var r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha; var max = Math.max(r, g, b), min = Math.min(r, g, b); var h, s, l = (max + min) / 2, d = max - min; if (max === min) { h = s = 0; } else { s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s, l: l, a: a }; }, //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript toHSV: function () { var r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha; var max = Math.max(r, g, b), min = Math.min(r, g, b); var h, s, v = max; var d = max - min; if (max === 0) { s = 0; } else { s = d / max; } if (max === min) { h = 0; } else { switch(max){ case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s, v: v, a: a }; }, toARGB: function () { return toHex([this.alpha * 255].concat(this.rgb)); }, compare: function (x) { if (!x.rgb) { return -1; } return (x.rgb[0] === this.rgb[0] && x.rgb[1] === this.rgb[1] && x.rgb[2] === this.rgb[2] && x.alpha === this.alpha) ? 0 : -1; } }; tree.Color.fromKeyword = function(keyword) { keyword = keyword.toLowerCase(); if (tree.colors.hasOwnProperty(keyword)) { // detect named color return new(tree.Color)(tree.colors[keyword].slice(1)); } if (keyword === transparentKeyword) { var transparent = new(tree.Color)([0, 0, 0], 0); transparent.isTransparentKeyword = true; return transparent; } }; function toHex(v) { return '#' + v.map(function (c) { c = clamp(Math.round(c), 255); return (c < 16 ? '0' : '') + c.toString(16); }).join(''); } function clamp(v, max) { return Math.min(Math.max(v, 0), max); } })(require('../tree')); (function (tree) { tree.Comment = function (value, silent, index, currentFileInfo) { this.value = value; this.silent = !!silent; this.currentFileInfo = currentFileInfo; }; tree.Comment.prototype = { type: "Comment", genCSS: function (env, output) { if (this.debugInfo) { output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index); } output.add(this.value.trim()); //TODO shouldn't need to trim, we shouldn't grab the \n }, toCSS: tree.toCSS, isSilent: function(env) { var isReference = (this.currentFileInfo && this.currentFileInfo.reference && !this.isReferenced), isCompressed = env.compress && !this.value.match(/^\/\*!/); return this.silent || isReference || isCompressed; }, eval: function () { return this; }, markReferenced: function () { this.isReferenced = true; } }; })(require('../tree')); (function (tree) { tree.Condition = function (op, l, r, i, negate) { this.op = op.trim(); this.lvalue = l; this.rvalue = r; this.index = i; this.negate = negate; }; tree.Condition.prototype = { type: "Condition", accept: function (visitor) { this.lvalue = visitor.visit(this.lvalue); this.rvalue = visitor.visit(this.rvalue); }, eval: function (env) { var a = this.lvalue.eval(env), b = this.rvalue.eval(env); var i = this.index, result; result = (function (op) { switch (op) { case 'and': return a && b; case 'or': return a || b; default: if (a.compare) { result = a.compare(b); } else if (b.compare) { result = b.compare(a); } else { throw { type: "Type", message: "Unable to perform comparison", index: i }; } switch (result) { case -1: return op === '<' || op === '=<' || op === '<='; case 0: return op === '=' || op === '>=' || op === '=<' || op === '<='; case 1: return op === '>' || op === '>='; } } })(this.op); return this.negate ? !result : result; } }; })(require('../tree')); (function (tree) { tree.DetachedRuleset = function (ruleset, frames) { this.ruleset = ruleset; this.frames = frames; }; tree.DetachedRuleset.prototype = { type: "DetachedRuleset", accept: function (visitor) { this.ruleset = visitor.visit(this.ruleset); }, eval: function (env) { var frames = this.frames || env.frames.slice(0); return new tree.DetachedRuleset(this.ruleset, frames); }, callEval: function (env) { return this.ruleset.eval(this.frames ? new(tree.evalEnv)(env, this.frames.concat(env.frames)) : env); } }; })(require('../tree')); (function (tree) { // // A number with a unit // tree.Dimension = function (value, unit) { this.value = parseFloat(value); this.unit = (unit && unit instanceof tree.Unit) ? unit : new(tree.Unit)(unit ? [unit] : undefined); }; tree.Dimension.prototype = { type: "Dimension", accept: function (visitor) { this.unit = visitor.visit(this.unit); }, eval: function (env) { return this; }, toColor: function () { return new(tree.Color)([this.value, this.value, this.value]); }, genCSS: function (env, output) { if ((env && env.strictUnits) && !this.unit.isSingular()) { throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString()); } var value = tree.fround(env, this.value), strValue = String(value); if (value !== 0 && value < 0.000001 && value > -0.000001) { // would be output 1e-6 etc. strValue = value.toFixed(20).replace(/0+$/, ""); } if (env && env.compress) { // Zero values doesn't need a unit if (value === 0 && this.unit.isLength()) { output.add(strValue); return; } // Float values doesn't need a leading zero if (value > 0 && value < 1) { strValue = (strValue).substr(1); } } output.add(strValue); this.unit.genCSS(env, output); }, toCSS: tree.toCSS, // In an operation between two Dimensions, // we default to the first Dimension's unit, // so `1px + 2` will yield `3px`. operate: function (env, op, other) { /*jshint noempty:false */ var value = tree.operate(env, op, this.value, other.value), unit = this.unit.clone(); if (op === '+' || op === '-') { if (unit.numerator.length === 0 && unit.denominator.length === 0) { unit.numerator = other.unit.numerator.slice(0); unit.denominator = other.unit.denominator.slice(0); } else if (other.unit.numerator.length === 0 && unit.denominator.length === 0) { // do nothing } else { other = other.convertTo(this.unit.usedUnits()); if(env.strictUnits && other.unit.toString() !== unit.toString()) { throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '" + unit.toString() + "' and '" + other.unit.toString() + "'."); } value = tree.operate(env, op, this.value, other.value); } } else if (op === '*') { unit.numerator = unit.numerator.concat(other.unit.numerator).sort(); unit.denominator = unit.denominator.concat(other.unit.denominator).sort(); unit.cancel(); } else if (op === '/') { unit.numerator = unit.numerator.concat(other.unit.denominator).sort(); unit.denominator = unit.denominator.concat(other.unit.numerator).sort(); unit.cancel(); } return new(tree.Dimension)(value, unit); }, compare: function (other) { if (other instanceof tree.Dimension) { var a, b, aValue, bValue; if (this.unit.isEmpty() || other.unit.isEmpty()) { a = this; b = other; } else { a = this.unify(); b = other.unify(); if (a.unit.compare(b.unit) !== 0) { return -1; } } aValue = a.value; bValue = b.value; if (bValue > aValue) { return -1; } else if (bValue < aValue) { return 1; } else { return 0; } } else { return -1; } }, unify: function () { return this.convertTo({ length: 'px', duration: 's', angle: 'rad' }); }, convertTo: function (conversions) { var value = this.value, unit = this.unit.clone(), i, groupName, group, targetUnit, derivedConversions = {}, applyUnit; if (typeof conversions === 'string') { for(i in tree.UnitConversions) { if (tree.UnitConversions[i].hasOwnProperty(conversions)) { derivedConversions = {}; derivedConversions[i] = conversions; } } conversions = derivedConversions; } applyUnit = function (atomicUnit, denominator) { /*jshint loopfunc:true */ if (group.hasOwnProperty(atomicUnit)) { if (denominator) { value = value / (group[atomicUnit] / group[targetUnit]); } else { value = value * (group[atomicUnit] / group[targetUnit]); } return targetUnit; } return atomicUnit; }; for (groupName in conversions) { if (conversions.hasOwnProperty(groupName)) { targetUnit = conversions[groupName]; group = tree.UnitConversions[groupName]; unit.map(applyUnit); } } unit.cancel(); return new(tree.Dimension)(value, unit); } }; // http://www.w3.org/TR/css3-values/#absolute-lengths tree.UnitConversions = { length: { 'm': 1, 'cm': 0.01, 'mm': 0.001, 'in': 0.0254, 'px': 0.0254 / 96, 'pt': 0.0254 / 72, 'pc': 0.0254 / 72 * 12 }, duration: { 's': 1, 'ms': 0.001 }, angle: { 'rad': 1/(2*Math.PI), 'deg': 1/360, 'grad': 1/400, 'turn': 1 } }; tree.Unit = function (numerator, denominator, backupUnit) { this.numerator = numerator ? numerator.slice(0).sort() : []; this.denominator = denominator ? denominator.slice(0).sort() : []; this.backupUnit = backupUnit; }; tree.Unit.prototype = { type: "Unit", clone: function () { return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit); }, genCSS: function (env, output) { if (this.numerator.length >= 1) { output.add(this.numerator[0]); } else if (this.denominator.length >= 1) { output.add(this.denominator[0]); } else if ((!env || !env.strictUnits) && this.backupUnit) { output.add(this.backupUnit); } }, toCSS: tree.toCSS, toString: function () { var i, returnStr = this.numerator.join("*"); for (i = 0; i < this.denominator.length; i++) { returnStr += "/" + this.denominator[i]; } return returnStr; }, compare: function (other) { return this.is(other.toString()) ? 0 : -1; }, is: function (unitString) { return this.toString() === unitString; }, isLength: function () { return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/)); }, isEmpty: function () { return this.numerator.length === 0 && this.denominator.length === 0; }, isSingular: function() { return this.numerator.length <= 1 && this.denominator.length === 0; }, map: function(callback) { var i; for (i = 0; i < this.numerator.length; i++) { this.numerator[i] = callback(this.numerator[i], false); } for (i = 0; i < this.denominator.length; i++) { this.denominator[i] = callback(this.denominator[i], true); } }, usedUnits: function() { var group, result = {}, mapUnit; mapUnit = function (atomicUnit) { /*jshint loopfunc:true */ if (group.hasOwnProperty(atomicUnit) && !result[groupName]) { result[groupName] = atomicUnit; } return atomicUnit; }; for (var groupName in tree.UnitConversions) { if (tree.UnitConversions.hasOwnProperty(groupName)) { group = tree.UnitConversions[groupName]; this.map(mapUnit); } } return result; }, cancel: function () { var counter = {}, atomicUnit, i, backup; for (i = 0; i < this.numerator.length; i++) { atomicUnit = this.numerator[i]; if (!backup) { backup = atomicUnit; } counter[atomicUnit] = (counter[atomicUnit] || 0) + 1; } for (i = 0; i < this.denominator.length; i++) { atomicUnit = this.denominator[i]; if (!backup) { backup = atomicUnit; } counter[atomicUnit] = (counter[atomicUnit] || 0) - 1; } this.numerator = []; this.denominator = []; for (atomicUnit in counter) { if (counter.hasOwnProperty(atomicUnit)) { var count = counter[atomicUnit]; if (count > 0) { for (i = 0; i < count; i++) { this.numerator.push(atomicUnit); } } else if (count < 0) { for (i = 0; i < -count; i++) { this.denominator.push(atomicUnit); } } } } if (this.numerator.length === 0 && this.denominator.length === 0 && backup) { this.backupUnit = backup; } this.numerator.sort(); this.denominator.sort(); } }; })(require('../tree')); (function (tree) { tree.Directive = function (name, value, rules, index, currentFileInfo, debugInfo) { this.name = name; this.value = value; if (rules) { this.rules = rules; this.rules.allowImports = true; } this.index = index; this.currentFileInfo = currentFileInfo; this.debugInfo = debugInfo; }; tree.Directive.prototype = { type: "Directive", accept: function (visitor) { var value = this.value, rules = this.rules; if (rules) { rules = visitor.visit(rules); } if (value) { value = visitor.visit(value); } }, genCSS: function (env, output) { var value = this.value, rules = this.rules; output.add(this.name, this.currentFileInfo, this.index); if (value) { output.add(' '); value.genCSS(env, output); } if (rules) { tree.outputRuleset(env, output, [rules]); } else { output.add(';'); } }, toCSS: tree.toCSS, eval: function (env) { var value = this.value, rules = this.rules; if (value) { value = value.eval(env); } if (rules) { rules = rules.eval(env); rules.root = true; } return new(tree.Directive)(this.name, value, rules, this.index, this.currentFileInfo, this.debugInfo); }, variable: function (name) { if (this.rules) return tree.Ruleset.prototype.variable.call(this.rules, name); }, find: function () { if (this.rules) return tree.Ruleset.prototype.find.apply(this.rules, arguments); }, rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); }, markReferenced: function () { var i, rules; this.isReferenced = true; if (this.rules) { rules = this.rules.rules; for (i = 0; i < rules.length; i++) { if (rules[i].markReferenced) { rules[i].markReferenced(); } } } } }; })(require('../tree')); (function (tree) { tree.Element = function (combinator, value, index, currentFileInfo) { this.combinator = combinator instanceof tree.Combinator ? combinator : new(tree.Combinator)(combinator); if (typeof(value) === 'string') { this.value = value.trim(); } else if (value) { this.value = value; } else { this.value = ""; } this.index = index; this.currentFileInfo = currentFileInfo; }; tree.Element.prototype = { type: "Element", accept: function (visitor) { var value = this.value; this.combinator = visitor.visit(this.combinator); if (typeof value === "object") { this.value = visitor.visit(value); } }, eval: function (env) { return new(tree.Element)(this.combinator, this.value.eval ? this.value.eval(env) : this.value, this.index, this.currentFileInfo); }, genCSS: function (env, output) { output.add(this.toCSS(env), this.currentFileInfo, this.index); }, toCSS: function (env) { var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); if (value === '' && this.combinator.value.charAt(0) === '&') { return ''; } else { return this.combinator.toCSS(env || {}) + value; } } }; tree.Attribute = function (key, op, value) { this.key = key; this.op = op; this.value = value; }; tree.Attribute.prototype = { type: "Attribute", eval: function (env) { return new(tree.Attribute)(this.key.eval ? this.key.eval(env) : this.key, this.op, (this.value && this.value.eval) ? this.value.eval(env) : this.value); }, genCSS: function (env, output) { output.add(this.toCSS(env)); }, toCSS: function (env) { var value = this.key.toCSS ? this.key.toCSS(env) : this.key; if (this.op) { value += this.op; value += (this.value.toCSS ? this.value.toCSS(env) : this.value); } return '[' + value + ']'; } }; tree.Combinator = function (value) { if (value === ' ') { this.value = ' '; } else { this.value = value ? value.trim() : ""; } }; tree.Combinator.prototype = { type: "Combinator", _outputMap: { '' : '', ' ' : ' ', ':' : ' :', '+' : ' + ', '~' : ' ~ ', '>' : ' > ', '|' : '|', '^' : ' ^ ', '^^' : ' ^^ ' }, _outputMapCompressed: { '' : '', ' ' : ' ', ':' : ' :', '+' : '+', '~' : '~', '>' : '>', '|' : '|', '^' : '^', '^^' : '^^' }, genCSS: function (env, output) { output.add((env.compress ? this._outputMapCompressed : this._outputMap)[this.value]); }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { tree.Expression = function (value) { this.value = value; }; tree.Expression.prototype = { type: "Expression", accept: function (visitor) { if (this.value) { this.value = visitor.visitArray(this.value); } }, eval: function (env) { var returnValue, inParenthesis = this.parens && !this.parensInOp, doubleParen = false; if (inParenthesis) { env.inParenthesis(); } if (this.value.length > 1) { returnValue = new(tree.Expression)(this.value.map(function (e) { return e.eval(env); })); } else if (this.value.length === 1) { if (this.value[0].parens && !this.value[0].parensInOp) { doubleParen = true; } returnValue = this.value[0].eval(env); } else { returnValue = this; } if (inParenthesis) { env.outOfParenthesis(); } if (this.parens && this.parensInOp && !(env.isMathOn()) && !doubleParen) { returnValue = new(tree.Paren)(returnValue); } return returnValue; }, genCSS: function (env, output) { for(var i = 0; i < this.value.length; i++) { this.value[i].genCSS(env, output); if (i + 1 < this.value.length) { output.add(" "); } } }, toCSS: tree.toCSS, throwAwayComments: function () { this.value = this.value.filter(function(v) { return !(v instanceof tree.Comment); }); } }; })(require('../tree')); (function (tree) { tree.Extend = function Extend(selector, option, index) { this.selector = selector; this.option = option; this.index = index; this.object_id = tree.Extend.next_id++; this.parent_ids = [this.object_id]; switch(option) { case "all": this.allowBefore = true; this.allowAfter = true; break; default: this.allowBefore = false; this.allowAfter = false; break; } }; tree.Extend.next_id = 0; tree.Extend.prototype = { type: "Extend", accept: function (visitor) { this.selector = visitor.visit(this.selector); }, eval: function (env) { return new(tree.Extend)(this.selector.eval(env), this.option, this.index); }, clone: function (env) { return new(tree.Extend)(this.selector, this.option, this.index); }, findSelfSelectors: function (selectors) { var selfElements = [], i, selectorElements; for(i = 0; i < selectors.length; i++) { selectorElements = selectors[i].elements; // duplicate the logic in genCSS function inside the selector node. // future TODO - move both logics into the selector joiner visitor if (i > 0 && selectorElements.length && selectorElements[0].combinator.value === "") { selectorElements[0].combinator.value = ' '; } selfElements = selfElements.concat(selectors[i].elements); } this.selfSelectors = [{ elements: selfElements }]; } }; })(require('../tree')); (function (tree) { // // CSS @import node // // The general strategy here is that we don't want to wait // for the parsing to be completed, before we start importing // the file. That's because in the context of a browser, // most of the time will be spent waiting for the server to respond. // // On creation, we push the import path to our import queue, though // `import,push`, we also pass it a callback, which it'll call once // the file has been fetched, and parsed. // tree.Import = function (path, features, options, index, currentFileInfo) { this.options = options; this.index = index; this.path = path; this.features = features; this.currentFileInfo = currentFileInfo; if (this.options.less !== undefined || this.options.inline) { this.css = !this.options.less || this.options.inline; } else { var pathValue = this.getPath(); if (pathValue && /css([\?;].*)?$/.test(pathValue)) { this.css = true; } } }; // // The actual import node doesn't return anything, when converted to CSS. // The reason is that it's used at the evaluation stage, so that the rules // it imports can be treated like any other rules. // // In `eval`, we make sure all Import nodes get evaluated, recursively, so // we end up with a flat structure, which can easily be imported in the parent // ruleset. // tree.Import.prototype = { type: "Import", accept: function (visitor) { if (this.features) { this.features = visitor.visit(this.features); } this.path = visitor.visit(this.path); if (!this.options.inline && this.root) { this.root = visitor.visit(this.root); } }, genCSS: function (env, output) { if (this.css) { output.add("@import ", this.currentFileInfo, this.index); this.path.genCSS(env, output); if (this.features) { output.add(" "); this.features.genCSS(env, output); } output.add(';'); } }, toCSS: tree.toCSS, getPath: function () { if (this.path instanceof tree.Quoted) { var path = this.path.value; return (this.css !== undefined || /(\.[a-z]*$)|([\?;].*)$/.test(path)) ? path : path + '.less'; } else if (this.path instanceof tree.URL) { return this.path.value.value; } return null; }, evalForImport: function (env) { return new(tree.Import)(this.path.eval(env), this.features, this.options, this.index, this.currentFileInfo); }, evalPath: function (env) { var path = this.path.eval(env); var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath; if (!(path instanceof tree.URL)) { if (rootpath) { var pathValue = path.value; // Add the base path if the import is relative if (pathValue && env.isPathRelative(pathValue)) { path.value = rootpath +pathValue; } } path.value = env.normalizePath(path.value); } return path; }, eval: function (env) { var ruleset, features = this.features && this.features.eval(env); if (this.skip) { if (typeof this.skip === "function") { this.skip = this.skip(); } if (this.skip) { return []; } } if (this.options.inline) { //todo needs to reference css file not import var contents = new(tree.Anonymous)(this.root, 0, {filename: this.importedFilename}, true); return this.features ? new(tree.Media)([contents], this.features.value) : [contents]; } else if (this.css) { var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index); if (!newImport.css && this.error) { throw this.error; } return newImport; } else { ruleset = new(tree.Ruleset)(null, this.root.rules.slice(0)); ruleset.evalImports(env); return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; } } }; })(require('../tree')); (function (tree) { tree.JavaScript = function (string, index, escaped) { this.escaped = escaped; this.expression = string; this.index = index; }; tree.JavaScript.prototype = { type: "JavaScript", eval: function (env) { var result, that = this, context = {}; var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); }); try { expression = new(Function)('return (' + expression + ')'); } catch (e) { throw { message: "JavaScript evaluation error: " + e.message + " from `" + expression + "`" , index: this.index }; } var variables = env.frames[0].variables(); for (var k in variables) { if (variables.hasOwnProperty(k)) { /*jshint loopfunc:true */ context[k.slice(1)] = { value: variables[k].value, toJS: function () { return this.value.eval(env).toCSS(); } }; } } try { result = expression.call(context); } catch (e) { throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message.replace(/["]/g, "'") + "'" , index: this.index }; } if (typeof(result) === 'number') { return new(tree.Dimension)(result); } else if (typeof(result) === 'string') { return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); } else if (Array.isArray(result)) { return new(tree.Anonymous)(result.join(', ')); } else { return new(tree.Anonymous)(result); } } }; })(require('../tree')); (function (tree) { tree.Keyword = function (value) { this.value = value; }; tree.Keyword.prototype = { type: "Keyword", eval: function () { return this; }, genCSS: function (env, output) { if (this.value === '%') { throw { type: "Syntax", message: "Invalid % without number" }; } output.add(this.value); }, toCSS: tree.toCSS, compare: function (other) { if (other instanceof tree.Keyword) { return other.value === this.value ? 0 : 1; } else { return -1; } } }; tree.True = new(tree.Keyword)('true'); tree.False = new(tree.Keyword)('false'); })(require('../tree')); (function (tree) { tree.Media = function (value, features, index, currentFileInfo) { this.index = index; this.currentFileInfo = currentFileInfo; var selectors = this.emptySelectors(); this.features = new(tree.Value)(features); this.rules = [new(tree.Ruleset)(selectors, value)]; this.rules[0].allowImports = true; }; tree.Media.prototype = { type: "Media", accept: function (visitor) { if (this.features) { this.features = visitor.visit(this.features); } if (this.rules) { this.rules = visitor.visitArray(this.rules); } }, genCSS: function (env, output) { output.add('@media ', this.currentFileInfo, this.index); this.features.genCSS(env, output); tree.outputRuleset(env, output, this.rules); }, toCSS: tree.toCSS, eval: function (env) { if (!env.mediaBlocks) { env.mediaBlocks = []; env.mediaPath = []; } var media = new(tree.Media)(null, [], this.index, this.currentFileInfo); if(this.debugInfo) { this.rules[0].debugInfo = this.debugInfo; media.debugInfo = this.debugInfo; } var strictMathBypass = false; if (!env.strictMath) { strictMathBypass = true; env.strictMath = true; } try { media.features = this.features.eval(env); } finally { if (strictMathBypass) { env.strictMath = false; } } env.mediaPath.push(media); env.mediaBlocks.push(media); env.frames.unshift(this.rules[0]); media.rules = [this.rules[0].eval(env)]; env.frames.shift(); env.mediaPath.pop(); return env.mediaPath.length === 0 ? media.evalTop(env) : media.evalNested(env); }, variable: function (name) { return tree.Ruleset.prototype.variable.call(this.rules[0], name); }, find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); }, rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); }, emptySelectors: function() { var el = new(tree.Element)('', '&', this.index, this.currentFileInfo), sels = [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; sels[0].mediaEmpty = true; return sels; }, markReferenced: function () { var i, rules = this.rules[0].rules; this.rules[0].markReferenced(); this.isReferenced = true; for (i = 0; i < rules.length; i++) { if (rules[i].markReferenced) { rules[i].markReferenced(); } } }, evalTop: function (env) { var result = this; // Render all dependent Media blocks. if (env.mediaBlocks.length > 1) { var selectors = this.emptySelectors(); result = new(tree.Ruleset)(selectors, env.mediaBlocks); result.multiMedia = true; } delete env.mediaBlocks; delete env.mediaPath; return result; }, evalNested: function (env) { var i, value, path = env.mediaPath.concat([this]); // Extract the media-query conditions separated with `,` (OR). for (i = 0; i < path.length; i++) { value = path[i].features instanceof tree.Value ? path[i].features.value : path[i].features; path[i] = Array.isArray(value) ? value : [value]; } // Trace all permutations to generate the resulting media-query. // // (a, b and c) with nested (d, e) -> // a and d // a and e // b and c and d // b and c and e this.features = new(tree.Value)(this.permute(path).map(function (path) { path = path.map(function (fragment) { return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); }); for(i = path.length - 1; i > 0; i--) { path.splice(i, 0, new(tree.Anonymous)("and")); } return new(tree.Expression)(path); })); // Fake a tree-node that doesn't output anything. return new(tree.Ruleset)([], []); }, permute: function (arr) { if (arr.length === 0) { return []; } else if (arr.length === 1) { return arr[0]; } else { var result = []; var rest = this.permute(arr.slice(1)); for (var i = 0; i < rest.length; i++) { for (var j = 0; j < arr[0].length; j++) { result.push([arr[0][j]].concat(rest[i])); } } return result; } }, bubbleSelectors: function (selectors) { if (!selectors) return; this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])]; } }; })(require('../tree')); (function (tree) { tree.mixin = {}; tree.mixin.Call = function (elements, args, index, currentFileInfo, important) { this.selector = new(tree.Selector)(elements); this.arguments = (args && args.length) ? args : null; this.index = index; this.currentFileInfo = currentFileInfo; this.important = important; }; tree.mixin.Call.prototype = { type: "MixinCall", accept: function (visitor) { if (this.selector) { this.selector = visitor.visit(this.selector); } if (this.arguments) { this.arguments = visitor.visitArray(this.arguments); } }, eval: function (env) { var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound, rule, candidates = [], candidate, conditionResult = [], defaultFunc = tree.defaultFunc, defaultResult, defNone = 0, defTrue = 1, defFalse = 2, count; args = this.arguments && this.arguments.map(function (a) { return { name: a.name, value: a.value.eval(env) }; }); for (i = 0; i < env.frames.length; i++) { if ((mixins = env.frames[i].find(this.selector)).length > 0) { isOneFound = true; // To make `default()` function independent of definition order we have two "subpasses" here. // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`), // and build candidate list with corresponding flags. Then, when we know all possible matches, // we make a final decision. for (m = 0; m < mixins.length; m++) { mixin = mixins[m]; isRecursive = false; for(f = 0; f < env.frames.length; f++) { if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) { isRecursive = true; break; } } if (isRecursive) { continue; } if (mixin.matchArgs(args, env)) { candidate = {mixin: mixin, group: defNone}; if (mixin.matchCondition) { for (f = 0; f < 2; f++) { defaultFunc.value(f); conditionResult[f] = mixin.matchCondition(args, env); } if (conditionResult[0] || conditionResult[1]) { if (conditionResult[0] != conditionResult[1]) { candidate.group = conditionResult[1] ? defTrue : defFalse; } candidates.push(candidate); } } else { candidates.push(candidate); } match = true; } } defaultFunc.reset(); count = [0, 0, 0]; for (m = 0; m < candidates.length; m++) { count[candidates[m].group]++; } if (count[defNone] > 0) { defaultResult = defFalse; } else { defaultResult = defTrue; if ((count[defTrue] + count[defFalse]) > 1) { throw { type: 'Runtime', message: 'Ambiguous use of `default()` found when matching for `' + this.format(args) + '`', index: this.index, filename: this.currentFileInfo.filename }; } } for (m = 0; m < candidates.length; m++) { candidate = candidates[m].group; if ((candidate === defNone) || (candidate === defaultResult)) { try { mixin = candidates[m].mixin; if (!(mixin instanceof tree.mixin.Definition)) { mixin = new tree.mixin.Definition("", [], mixin.rules, null, false); mixin.originalRuleset = mixins[m].originalRuleset || mixins[m]; } Array.prototype.push.apply( rules, mixin.evalCall(env, args, this.important).rules); } catch (e) { throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack }; } } } if (match) { if (!this.currentFileInfo || !this.currentFileInfo.reference) { for (i = 0; i < rules.length; i++) { rule = rules[i]; if (rule.markReferenced) { rule.markReferenced(); } } } return rules; } } } if (isOneFound) { throw { type: 'Runtime', message: 'No matching definition was found for `' + this.format(args) + '`', index: this.index, filename: this.currentFileInfo.filename }; } else { throw { type: 'Name', message: this.selector.toCSS().trim() + " is undefined", index: this.index, filename: this.currentFileInfo.filename }; } }, format: function (args) { return this.selector.toCSS().trim() + '(' + (args ? args.map(function (a) { var argValue = ""; if (a.name) { argValue += a.name + ":"; } if (a.value.toCSS) { argValue += a.value.toCSS(); } else { argValue += "???"; } return argValue; }).join(', ') : "") + ")"; } }; tree.mixin.Definition = function (name, params, rules, condition, variadic, frames) { this.name = name; this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])]; this.params = params; this.condition = condition; this.variadic = variadic; this.arity = params.length; this.rules = rules; this._lookups = {}; this.required = params.reduce(function (count, p) { if (!p.name || (p.name && !p.value)) { return count + 1; } else { return count; } }, 0); this.parent = tree.Ruleset.prototype; this.frames = frames; }; tree.mixin.Definition.prototype = { type: "MixinDefinition", accept: function (visitor) { if (this.params && this.params.length) { this.params = visitor.visitArray(this.params); } this.rules = visitor.visitArray(this.rules); if (this.condition) { this.condition = visitor.visit(this.condition); } }, variable: function (name) { return this.parent.variable.call(this, name); }, variables: function () { return this.parent.variables.call(this); }, find: function () { return this.parent.find.apply(this, arguments); }, rulesets: function () { return this.parent.rulesets.apply(this); }, evalParams: function (env, mixinEnv, args, evaldArguments) { /*jshint boss:true */ var frame = new(tree.Ruleset)(null, null), varargs, arg, params = this.params.slice(0), i, j, val, name, isNamedFound, argIndex, argsLength = 0; mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames)); if (args) { args = args.slice(0); argsLength = args.length; for(i = 0; i < argsLength; i++) { arg = args[i]; if (name = (arg && arg.name)) { isNamedFound = false; for(j = 0; j < params.length; j++) { if (!evaldArguments[j] && name === params[j].name) { evaldArguments[j] = arg.value.eval(env); frame.prependRule(new(tree.Rule)(name, arg.value.eval(env))); isNamedFound = true; break; } } if (isNamedFound) { args.splice(i, 1); i--; continue; } else { throw { type: 'Runtime', message: "Named argument for " + this.name + ' ' + args[i].name + ' not found' }; } } } } argIndex = 0; for (i = 0; i < params.length; i++) { if (evaldArguments[i]) { continue; } arg = args && args[argIndex]; if (name = params[i].name) { if (params[i].variadic) { varargs = []; for (j = argIndex; j < argsLength; j++) { varargs.push(args[j].value.eval(env)); } frame.prependRule(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); } else { val = arg && arg.value; if (val) { val = val.eval(env); } else if (params[i].value) { val = params[i].value.eval(mixinEnv); frame.resetCache(); } else { throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + ' (' + argsLength + ' for ' + this.arity + ')' }; } frame.prependRule(new(tree.Rule)(name, val)); evaldArguments[i] = val; } } if (params[i].variadic && args) { for (j = argIndex; j < argsLength; j++) { evaldArguments[j] = args[j].value.eval(env); } } argIndex++; } return frame; }, eval: function (env) { return new tree.mixin.Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || env.frames.slice(0)); }, evalCall: function (env, args, important) { var _arguments = [], mixinFrames = this.frames ? this.frames.concat(env.frames) : env.frames, frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments), rules, ruleset; frame.prependRule(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); rules = this.rules.slice(0); ruleset = new(tree.Ruleset)(null, rules); ruleset.originalRuleset = this; ruleset = ruleset.eval(new(tree.evalEnv)(env, [this, frame].concat(mixinFrames))); if (important) { ruleset = this.parent.makeImportant.apply(ruleset); } return ruleset; }, matchCondition: function (args, env) { if (this.condition && !this.condition.eval( new(tree.evalEnv)(env, [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])] // the parameter variables .concat(this.frames) // the parent namespace/mixin frames .concat(env.frames)))) { // the current environment frames return false; } return true; }, matchArgs: function (args, env) { var argsLength = (args && args.length) || 0, len; if (! this.variadic) { if (argsLength < this.required) { return false; } if (argsLength > this.params.length) { return false; } } else { if (argsLength < (this.required - 1)) { return false; } } len = Math.min(argsLength, this.arity); for (var i = 0; i < len; i++) { if (!this.params[i].name && !this.params[i].variadic) { if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { return false; } } } return true; } }; })(require('../tree')); (function (tree) { tree.Negative = function (node) { this.value = node; }; tree.Negative.prototype = { type: "Negative", accept: function (visitor) { this.value = visitor.visit(this.value); }, genCSS: function (env, output) { output.add('-'); this.value.genCSS(env, output); }, toCSS: tree.toCSS, eval: function (env) { if (env.isMathOn()) { return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env); } return new(tree.Negative)(this.value.eval(env)); } }; })(require('../tree')); (function (tree) { tree.Operation = function (op, operands, isSpaced) { this.op = op.trim(); this.operands = operands; this.isSpaced = isSpaced; }; tree.Operation.prototype = { type: "Operation", accept: function (visitor) { this.operands = visitor.visit(this.operands); }, eval: function (env) { var a = this.operands[0].eval(env), b = this.operands[1].eval(env); if (env.isMathOn()) { if (a instanceof tree.Dimension && b instanceof tree.Color) { a = a.toColor(); } if (b instanceof tree.Dimension && a instanceof tree.Color) { b = b.toColor(); } if (!a.operate) { throw { type: "Operation", message: "Operation on an invalid type" }; } return a.operate(env, this.op, b); } else { return new(tree.Operation)(this.op, [a, b], this.isSpaced); } }, genCSS: function (env, output) { this.operands[0].genCSS(env, output); if (this.isSpaced) { output.add(" "); } output.add(this.op); if (this.isSpaced) { output.add(" "); } this.operands[1].genCSS(env, output); }, toCSS: tree.toCSS }; tree.operate = function (env, op, a, b) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b; } }; })(require('../tree')); (function (tree) { tree.Paren = function (node) { this.value = node; }; tree.Paren.prototype = { type: "Paren", accept: function (visitor) { this.value = visitor.visit(this.value); }, genCSS: function (env, output) { output.add('('); this.value.genCSS(env, output); output.add(')'); }, toCSS: tree.toCSS, eval: function (env) { return new(tree.Paren)(this.value.eval(env)); } }; })(require('../tree')); (function (tree) { tree.Quoted = function (str, content, escaped, index, currentFileInfo) { this.escaped = escaped; this.value = content || ''; this.quote = str.charAt(0); this.index = index; this.currentFileInfo = currentFileInfo; }; tree.Quoted.prototype = { type: "Quoted", genCSS: function (env, output) { if (!this.escaped) { output.add(this.quote, this.currentFileInfo, this.index); } output.add(this.value); if (!this.escaped) { output.add(this.quote); } }, toCSS: tree.toCSS, eval: function (env) { var that = this; var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { return new(tree.JavaScript)(exp, that.index, true).eval(env).value; }).replace(/@\{([\w-]+)\}/g, function (_, name) { var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true); return (v instanceof tree.Quoted) ? v.value : v.toCSS(); }); return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index, this.currentFileInfo); }, compare: function (x) { if (!x.toCSS) { return -1; } var left = this.toCSS(), right = x.toCSS(); if (left === right) { return 0; } return left < right ? -1 : 1; } }; })(require('../tree')); (function (tree) { tree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) { this.name = name; this.value = (value instanceof tree.Value || value instanceof tree.Ruleset) ? value : new(tree.Value)([value]); this.important = important ? ' ' + important.trim() : ''; this.merge = merge; this.index = index; this.currentFileInfo = currentFileInfo; this.inline = inline || false; this.variable = name.charAt && (name.charAt(0) === '@'); }; tree.Rule.prototype = { type: "Rule", accept: function (visitor) { this.value = visitor.visit(this.value); }, genCSS: function (env, output) { output.add(this.name + (env.compress ? ':' : ': '), this.currentFileInfo, this.index); try { this.value.genCSS(env, output); } catch(e) { e.index = this.index; e.filename = this.currentFileInfo.filename; throw e; } output.add(this.important + ((this.inline || (env.lastRule && env.compress)) ? "" : ";"), this.currentFileInfo, this.index); }, toCSS: tree.toCSS, eval: function (env) { var strictMathBypass = false, name = this.name, evaldValue; if (typeof name !== "string") { // expand 'primitive' name directly to get // things faster (~10% for benchmark.less): name = (name.length === 1) && (name[0] instanceof tree.Keyword) ? name[0].value : evalName(env, name); } if (name === "font" && !env.strictMath) { strictMathBypass = true; env.strictMath = true; } try { evaldValue = this.value.eval(env); if (!this.variable && evaldValue.type === "DetachedRuleset") { throw { message: "Rulesets cannot be evaluated on a property.", index: this.index, filename: this.currentFileInfo.filename }; } return new(tree.Rule)(name, evaldValue, this.important, this.merge, this.index, this.currentFileInfo, this.inline); } catch(e) { if (typeof e.index !== 'number') { e.index = this.index; e.filename = this.currentFileInfo.filename; } throw e; } finally { if (strictMathBypass) { env.strictMath = false; } } }, makeImportant: function () { return new(tree.Rule)(this.name, this.value, "!important", this.merge, this.index, this.currentFileInfo, this.inline); } }; function evalName(env, name) { var value = "", i, n = name.length, output = {add: function (s) {value += s;}}; for (i = 0; i < n; i++) { name[i].eval(env).genCSS(env, output); } return value; } })(require('../tree')); (function (tree) { tree.RulesetCall = function (variable) { this.variable = variable; }; tree.RulesetCall.prototype = { type: "RulesetCall", accept: function (visitor) { }, eval: function (env) { var detachedRuleset = new(tree.Variable)(this.variable).eval(env); return detachedRuleset.callEval(env); } }; })(require('../tree')); (function (tree) { tree.Ruleset = function (selectors, rules, strictImports) { this.selectors = selectors; this.rules = rules; this._lookups = {}; this.strictImports = strictImports; }; tree.Ruleset.prototype = { type: "Ruleset", accept: function (visitor) { if (this.paths) { visitor.visitArray(this.paths, true); } else if (this.selectors) { this.selectors = visitor.visitArray(this.selectors); } if (this.rules && this.rules.length) { this.rules = visitor.visitArray(this.rules); } }, eval: function (env) { var thisSelectors = this.selectors, selectors, selCnt, selector, i, defaultFunc = tree.defaultFunc, hasOnePassingSelector = false; if (thisSelectors && (selCnt = thisSelectors.length)) { selectors = []; defaultFunc.error({ type: "Syntax", message: "it is currently only allowed in parametric mixin guards," }); for (i = 0; i < selCnt; i++) { selector = thisSelectors[i].eval(env); selectors.push(selector); if (selector.evaldCondition) { hasOnePassingSelector = true; } } defaultFunc.reset(); } else { hasOnePassingSelector = true; } var rules = this.rules ? this.rules.slice(0) : null, ruleset = new(tree.Ruleset)(selectors, rules, this.strictImports), rule, subRule; ruleset.originalRuleset = this; ruleset.root = this.root; ruleset.firstRoot = this.firstRoot; ruleset.allowImports = this.allowImports; if(this.debugInfo) { ruleset.debugInfo = this.debugInfo; } if (!hasOnePassingSelector) { rules.length = 0; } // push the current ruleset to the frames stack var envFrames = env.frames; envFrames.unshift(ruleset); // currrent selectors var envSelectors = env.selectors; if (!envSelectors) { env.selectors = envSelectors = []; } envSelectors.unshift(this.selectors); // Evaluate imports if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { ruleset.evalImports(env); } // Store the frames around mixin definitions, // so they can be evaluated like closures when the time comes. var rsRules = ruleset.rules, rsRuleCnt = rsRules ? rsRules.length : 0; for (i = 0; i < rsRuleCnt; i++) { if (rsRules[i] instanceof tree.mixin.Definition || rsRules[i] instanceof tree.DetachedRuleset) { rsRules[i] = rsRules[i].eval(env); } } var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0; // Evaluate mixin calls. for (i = 0; i < rsRuleCnt; i++) { if (rsRules[i] instanceof tree.mixin.Call) { /*jshint loopfunc:true */ rules = rsRules[i].eval(env).filter(function(r) { if ((r instanceof tree.Rule) && r.variable) { // do not pollute the scope if the variable is // already there. consider returning false here // but we need a way to "return" variable from mixins return !(ruleset.variable(r.name)); } return true; }); rsRules.splice.apply(rsRules, [i, 1].concat(rules)); rsRuleCnt += rules.length - 1; i += rules.length-1; ruleset.resetCache(); } else if (rsRules[i] instanceof tree.RulesetCall) { /*jshint loopfunc:true */ rules = rsRules[i].eval(env).rules.filter(function(r) { if ((r instanceof tree.Rule) && r.variable) { // do not pollute the scope at all return false; } return true; }); rsRules.splice.apply(rsRules, [i, 1].concat(rules)); rsRuleCnt += rules.length - 1; i += rules.length-1; ruleset.resetCache(); } } // Evaluate everything else for (i = 0; i < rsRules.length; i++) { rule = rsRules[i]; if (! (rule instanceof tree.mixin.Definition || rule instanceof tree.DetachedRuleset)) { rsRules[i] = rule = rule.eval ? rule.eval(env) : rule; } } // Evaluate everything else for (i = 0; i < rsRules.length; i++) { rule = rsRules[i]; // for rulesets, check if it is a css guard and can be removed if (rule instanceof tree.Ruleset && rule.selectors && rule.selectors.length === 1) { // check if it can be folded in (e.g. & where) if (rule.selectors[0].isJustParentSelector()) { rsRules.splice(i--, 1); for(var j = 0; j < rule.rules.length; j++) { subRule = rule.rules[j]; if (!(subRule instanceof tree.Rule) || !subRule.variable) { rsRules.splice(++i, 0, subRule); } } } } } // Pop the stack envFrames.shift(); envSelectors.shift(); if (env.mediaBlocks) { for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) { env.mediaBlocks[i].bubbleSelectors(selectors); } } return ruleset; }, evalImports: function(env) { var rules = this.rules, i, importRules; if (!rules) { return; } for (i = 0; i < rules.length; i++) { if (rules[i] instanceof tree.Import) { importRules = rules[i].eval(env); if (importRules && importRules.length) { rules.splice.apply(rules, [i, 1].concat(importRules)); i+= importRules.length-1; } else { rules.splice(i, 1, importRules); } this.resetCache(); } } }, makeImportant: function() { return new tree.Ruleset(this.selectors, this.rules.map(function (r) { if (r.makeImportant) { return r.makeImportant(); } else { return r; } }), this.strictImports); }, matchArgs: function (args) { return !args || args.length === 0; }, // lets you call a css selector with a guard matchCondition: function (args, env) { var lastSelector = this.selectors[this.selectors.length-1]; if (!lastSelector.evaldCondition) { return false; } if (lastSelector.condition && !lastSelector.condition.eval( new(tree.evalEnv)(env, env.frames))) { return false; } return true; }, resetCache: function () { this._rulesets = null; this._variables = null; this._lookups = {}; }, variables: function () { if (!this._variables) { this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) { if (r instanceof tree.Rule && r.variable === true) { hash[r.name] = r; } return hash; }, {}); } return this._variables; }, variable: function (name) { return this.variables()[name]; }, rulesets: function () { if (!this.rules) { return null; } var _Ruleset = tree.Ruleset, _MixinDefinition = tree.mixin.Definition, filtRules = [], rules = this.rules, cnt = rules.length, i, rule; for (i = 0; i < cnt; i++) { rule = rules[i]; if ((rule instanceof _Ruleset) || (rule instanceof _MixinDefinition)) { filtRules.push(rule); } } return filtRules; }, prependRule: function (rule) { var rules = this.rules; if (rules) { rules.unshift(rule); } else { this.rules = [ rule ]; } }, find: function (selector, self) { self = self || this; var rules = [], match, key = selector.toCSS(); if (key in this._lookups) { return this._lookups[key]; } this.rulesets().forEach(function (rule) { if (rule !== self) { for (var j = 0; j < rule.selectors.length; j++) { match = selector.match(rule.selectors[j]); if (match) { if (selector.elements.length > match) { Array.prototype.push.apply(rules, rule.find( new(tree.Selector)(selector.elements.slice(match)), self)); } else { rules.push(rule); } break; } } } }); this._lookups[key] = rules; return rules; }, genCSS: function (env, output) { var i, j, ruleNodes = [], rulesetNodes = [], rulesetNodeCnt, debugInfo, // Line number debugging rule, path; env.tabLevel = (env.tabLevel || 0); if (!this.root) { env.tabLevel++; } var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(" "), tabSetStr = env.compress ? '' : Array(env.tabLevel).join(" "), sep; for (i = 0; i < this.rules.length; i++) { rule = this.rules[i]; if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive || (this.root && rule instanceof tree.Comment)) { rulesetNodes.push(rule); } else { ruleNodes.push(rule); } } // If this is the root node, we don't render // a selector, or {}. if (!this.root) { debugInfo = tree.debugInfo(env, this, tabSetStr); if (debugInfo) { output.add(debugInfo); output.add(tabSetStr); } var paths = this.paths, pathCnt = paths.length, pathSubCnt; sep = env.compress ? ',' : (',\n' + tabSetStr); for (i = 0; i < pathCnt; i++) { path = paths[i]; if (!(pathSubCnt = path.length)) { continue; } if (i > 0) { output.add(sep); } env.firstSelector = true; path[0].genCSS(env, output); env.firstSelector = false; for (j = 1; j < pathSubCnt; j++) { path[j].genCSS(env, output); } } output.add((env.compress ? '{' : ' {\n') + tabRuleStr); } // Compile rules and rulesets for (i = 0; i < ruleNodes.length; i++) { rule = ruleNodes[i]; // @page{ directive ends up with root elements inside it, a mix of rules and rulesets // In this instance we do not know whether it is the last property if (i + 1 === ruleNodes.length && (!this.root || rulesetNodes.length === 0 || this.firstRoot)) { env.lastRule = true; } if (rule.genCSS) { rule.genCSS(env, output); } else if (rule.value) { output.add(rule.value.toString()); } if (!env.lastRule) { output.add(env.compress ? '' : ('\n' + tabRuleStr)); } else { env.lastRule = false; } } if (!this.root) { output.add((env.compress ? '}' : '\n' + tabSetStr + '}')); env.tabLevel--; } sep = (env.compress ? "" : "\n") + (this.root ? tabRuleStr : tabSetStr); rulesetNodeCnt = rulesetNodes.length; if (rulesetNodeCnt) { if (ruleNodes.length && sep) { output.add(sep); } rulesetNodes[0].genCSS(env, output); for (i = 1; i < rulesetNodeCnt; i++) { if (sep) { output.add(sep); } rulesetNodes[i].genCSS(env, output); } } if (!output.isEmpty() && !env.compress && this.firstRoot) { output.add('\n'); } }, toCSS: tree.toCSS, markReferenced: function () { if (!this.selectors) { return; } for (var s = 0; s < this.selectors.length; s++) { this.selectors[s].markReferenced(); } }, joinSelectors: function (paths, context, selectors) { for (var s = 0; s < selectors.length; s++) { this.joinSelector(paths, context, selectors[s]); } }, joinSelector: function (paths, context, selector) { var i, j, k, hasParentSelector, newSelectors, el, sel, parentSel, newSelectorPath, afterParentJoin, newJoinedSelector, newJoinedSelectorEmpty, lastSelector, currentElements, selectorsMultiplied; for (i = 0; i < selector.elements.length; i++) { el = selector.elements[i]; if (el.value === '&') { hasParentSelector = true; } } if (!hasParentSelector) { if (context.length > 0) { for (i = 0; i < context.length; i++) { paths.push(context[i].concat(selector)); } } else { paths.push([selector]); } return; } // The paths are [[Selector]] // The first list is a list of comma seperated selectors // The inner list is a list of inheritance seperated selectors // e.g. // .a, .b { // .c { // } // } // == [[.a] [.c]] [[.b] [.c]] // // the elements from the current selector so far currentElements = []; // the current list of new selectors to add to the path. // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors // by the parents newSelectors = [[]]; for (i = 0; i < selector.elements.length; i++) { el = selector.elements[i]; // non parent reference elements just get added if (el.value !== "&") { currentElements.push(el); } else { // the new list of selectors to add selectorsMultiplied = []; // merge the current list of non parent selector elements // on to the current list of selectors to add if (currentElements.length > 0) { this.mergeElementsOnToSelectors(currentElements, newSelectors); } // loop through our current selectors for (j = 0; j < newSelectors.length; j++) { sel = newSelectors[j]; // if we don't have any parent paths, the & might be in a mixin so that it can be used // whether there are parents or not if (context.length === 0) { // the combinator used on el should now be applied to the next element instead so that // it is not lost if (sel.length > 0) { sel[0].elements = sel[0].elements.slice(0); sel[0].elements.push(new(tree.Element)(el.combinator, '', el.index, el.currentFileInfo)); } selectorsMultiplied.push(sel); } else { // and the parent selectors for (k = 0; k < context.length; k++) { parentSel = context[k]; // We need to put the current selectors // then join the last selector's elements on to the parents selectors // our new selector path newSelectorPath = []; // selectors from the parent after the join afterParentJoin = []; newJoinedSelectorEmpty = true; //construct the joined selector - if & is the first thing this will be empty, // if not newJoinedSelector will be the last set of elements in the selector if (sel.length > 0) { newSelectorPath = sel.slice(0); lastSelector = newSelectorPath.pop(); newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0)); newJoinedSelectorEmpty = false; } else { newJoinedSelector = selector.createDerived([]); } //put together the parent selectors after the join if (parentSel.length > 1) { afterParentJoin = afterParentJoin.concat(parentSel.slice(1)); } if (parentSel.length > 0) { newJoinedSelectorEmpty = false; // join the elements so far with the first part of the parent newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, el.index, el.currentFileInfo)); newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); } if (!newJoinedSelectorEmpty) { // now add the joined selector newSelectorPath.push(newJoinedSelector); } // and the rest of the parent newSelectorPath = newSelectorPath.concat(afterParentJoin); // add that to our new set of selectors selectorsMultiplied.push(newSelectorPath); } } } // our new selectors has been multiplied, so reset the state newSelectors = selectorsMultiplied; currentElements = []; } } // if we have any elements left over (e.g. .a& .b == .b) // add them on to all the current selectors if (currentElements.length > 0) { this.mergeElementsOnToSelectors(currentElements, newSelectors); } for (i = 0; i < newSelectors.length; i++) { if (newSelectors[i].length > 0) { paths.push(newSelectors[i]); } } }, mergeElementsOnToSelectors: function(elements, selectors) { var i, sel; if (selectors.length === 0) { selectors.push([ new(tree.Selector)(elements) ]); return; } for (i = 0; i < selectors.length; i++) { sel = selectors[i]; // if the previous thing in sel is a parent this needs to join on to it if (sel.length > 0) { sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); } else { sel.push(new(tree.Selector)(elements)); } } } }; })(require('../tree')); (function (tree) { tree.Selector = function (elements, extendList, condition, index, currentFileInfo, isReferenced) { this.elements = elements; this.extendList = extendList; this.condition = condition; this.currentFileInfo = currentFileInfo || {}; this.isReferenced = isReferenced; if (!condition) { this.evaldCondition = true; } }; tree.Selector.prototype = { type: "Selector", accept: function (visitor) { if (this.elements) { this.elements = visitor.visitArray(this.elements); } if (this.extendList) { this.extendList = visitor.visitArray(this.extendList); } if (this.condition) { this.condition = visitor.visit(this.condition); } }, createDerived: function(elements, extendList, evaldCondition) { evaldCondition = (evaldCondition != null) ? evaldCondition : this.evaldCondition; var newSelector = new(tree.Selector)(elements, extendList || this.extendList, null, this.index, this.currentFileInfo, this.isReferenced); newSelector.evaldCondition = evaldCondition; newSelector.mediaEmpty = this.mediaEmpty; return newSelector; }, match: function (other) { var elements = this.elements, len = elements.length, olen, i; other.CacheElements(); olen = other._elements.length; if (olen === 0 || len < olen) { return 0; } else { for (i = 0; i < olen; i++) { if (elements[i].value !== other._elements[i]) { return 0; } } } return olen; // return number of matched elements }, CacheElements: function(){ var css = '', len, v, i; if( !this._elements ){ len = this.elements.length; for(i = 0; i < len; i++){ v = this.elements[i]; css += v.combinator.value; if( !v.value.value ){ css += v.value; continue; } if( typeof v.value.value !== "string" ){ css = ''; break; } css += v.value.value; } this._elements = css.match(/[,&#\.\w-]([\w-]|(\\.))*/g); if (this._elements) { if (this._elements[0] === "&") { this._elements.shift(); } } else { this._elements = []; } } }, isJustParentSelector: function() { return !this.mediaEmpty && this.elements.length === 1 && this.elements[0].value === '&' && (this.elements[0].combinator.value === ' ' || this.elements[0].combinator.value === ''); }, eval: function (env) { var evaldCondition = this.condition && this.condition.eval(env), elements = this.elements, extendList = this.extendList; elements = elements && elements.map(function (e) { return e.eval(env); }); extendList = extendList && extendList.map(function(extend) { return extend.eval(env); }); return this.createDerived(elements, extendList, evaldCondition); }, genCSS: function (env, output) { var i, element; if ((!env || !env.firstSelector) && this.elements[0].combinator.value === "") { output.add(' ', this.currentFileInfo, this.index); } if (!this._css) { //TODO caching? speed comparison? for(i = 0; i < this.elements.length; i++) { element = this.elements[i]; element.genCSS(env, output); } } }, toCSS: tree.toCSS, markReferenced: function () { this.isReferenced = true; }, getIsReferenced: function() { return !this.currentFileInfo.reference || this.isReferenced; }, getIsOutput: function() { return this.evaldCondition; } }; })(require('../tree')); (function (tree) { tree.UnicodeDescriptor = function (value) { this.value = value; }; tree.UnicodeDescriptor.prototype = { type: "UnicodeDescriptor", genCSS: function (env, output) { output.add(this.value); }, toCSS: tree.toCSS, eval: function () { return this; } }; })(require('../tree')); (function (tree) { tree.URL = function (val, currentFileInfo, isEvald) { this.value = val; this.currentFileInfo = currentFileInfo; this.isEvald = isEvald; }; tree.URL.prototype = { type: "Url", accept: function (visitor) { this.value = visitor.visit(this.value); }, genCSS: function (env, output) { output.add("url("); this.value.genCSS(env, output); output.add(")"); }, toCSS: tree.toCSS, eval: function (ctx) { var val = this.value.eval(ctx), rootpath; if (!this.isEvald) { // Add the base path if the URL is relative rootpath = this.currentFileInfo && this.currentFileInfo.rootpath; if (rootpath && typeof val.value === "string" && ctx.isPathRelative(val.value)) { if (!val.quote) { rootpath = rootpath.replace(/[\(\)'"\s]/g, function(match) { return "\\"+match; }); } val.value = rootpath + val.value; } val.value = ctx.normalizePath(val.value); // Add url args if enabled if (ctx.urlArgs) { if (!val.value.match(/^\s*data:/)) { var delimiter = val.value.indexOf('?') === -1 ? '?' : '&'; var urlArgs = delimiter + ctx.urlArgs; if (val.value.indexOf('#') !== -1) { val.value = val.value.replace('#', urlArgs + '#'); } else { val.value += urlArgs; } } } } return new(tree.URL)(val, this.currentFileInfo, true); } }; })(require('../tree')); (function (tree) { tree.Value = function (value) { this.value = value; }; tree.Value.prototype = { type: "Value", accept: function (visitor) { if (this.value) { this.value = visitor.visitArray(this.value); } }, eval: function (env) { if (this.value.length === 1) { return this.value[0].eval(env); } else { return new(tree.Value)(this.value.map(function (v) { return v.eval(env); })); } }, genCSS: function (env, output) { var i; for(i = 0; i < this.value.length; i++) { this.value[i].genCSS(env, output); if (i+1 < this.value.length) { output.add((env && env.compress) ? ',' : ', '); } } }, toCSS: tree.toCSS }; })(require('../tree')); (function (tree) { tree.Variable = function (name, index, currentFileInfo) { this.name = name; this.index = index; this.currentFileInfo = currentFileInfo || {}; }; tree.Variable.prototype = { type: "Variable", eval: function (env) { var variable, name = this.name; if (name.indexOf('@@') === 0) { name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; } if (this.evaluating) { throw { type: 'Name', message: "Recursive variable definition for " + name, filename: this.currentFileInfo.file, index: this.index }; } this.evaluating = true; variable = tree.find(env.frames, function (frame) { var v = frame.variable(name); if (v) { return v.value.eval(env); } }); if (variable) { this.evaluating = false; return variable; } else { throw { type: 'Name', message: "variable " + name + " is undefined", filename: this.currentFileInfo.filename, index: this.index }; } } }; })(require('../tree')); (function (tree) { var parseCopyProperties = [ 'paths', // option - unmodified - paths to search for imports on 'optimization', // option - optimization level (for the chunker) 'files', // list of files that have been imported, used for import-once 'contents', // map - filename to contents of all the files 'contentsIgnoredChars', // map - filename to lines at the begining of each file to ignore 'relativeUrls', // option - whether to adjust URL's to be relative 'rootpath', // option - rootpath to append to URL's 'strictImports', // option - 'insecure', // option - whether to allow imports from insecure ssl hosts 'dumpLineNumbers', // option - whether to dump line numbers 'compress', // option - whether to compress 'processImports', // option - whether to process imports. if false then imports will not be imported 'syncImport', // option - whether to import synchronously 'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true 'mime', // browser only - mime type for sheet import 'useFileCache', // browser only - whether to use the per file session cache 'currentFileInfo' // information about the current file - for error reporting and importing and making urls relative etc. ]; //currentFileInfo = { // 'relativeUrls' - option - whether to adjust URL's to be relative // 'filename' - full resolved filename of current file // 'rootpath' - path to append to normal URLs for this node // 'currentDirectory' - path to the current file, absolute // 'rootFilename' - filename of the base file // 'entryPath' - absolute path to the entry file // 'reference' - whether the file should not be output and only output parts that are referenced tree.parseEnv = function(options) { copyFromOriginal(options, this, parseCopyProperties); if (!this.contents) { this.contents = {}; } if (!this.contentsIgnoredChars) { this.contentsIgnoredChars = {}; } if (!this.files) { this.files = {}; } if (!this.currentFileInfo) { var filename = (options && options.filename) || "input"; var entryPath = filename.replace(/[^\/\\]*$/, ""); if (options) { options.filename = null; } this.currentFileInfo = { filename: filename, relativeUrls: this.relativeUrls, rootpath: (options && options.rootpath) || "", currentDirectory: entryPath, entryPath: entryPath, rootFilename: filename }; } }; var evalCopyProperties = [ 'silent', // whether to swallow errors and warnings 'verbose', // whether to log more activity 'compress', // whether to compress 'yuicompress', // whether to compress with the outside tool yui compressor 'ieCompat', // whether to enforce IE compatibility (IE8 data-uri) 'strictMath', // whether math has to be within parenthesis 'strictUnits', // whether units need to evaluate correctly 'cleancss', // whether to compress with clean-css 'sourceMap', // whether to output a source map 'importMultiple', // whether we are currently importing multiple copies 'urlArgs' // whether to add args into url tokens ]; tree.evalEnv = function(options, frames) { copyFromOriginal(options, this, evalCopyProperties); this.frames = frames || []; }; tree.evalEnv.prototype.inParenthesis = function () { if (!this.parensStack) { this.parensStack = []; } this.parensStack.push(true); }; tree.evalEnv.prototype.outOfParenthesis = function () { this.parensStack.pop(); }; tree.evalEnv.prototype.isMathOn = function () { return this.strictMath ? (this.parensStack && this.parensStack.length) : true; }; tree.evalEnv.prototype.isPathRelative = function (path) { return !/^(?:[a-z-]+:|\/)/.test(path); }; tree.evalEnv.prototype.normalizePath = function( path ) { var segments = path.split("/").reverse(), segment; path = []; while (segments.length !== 0 ) { segment = segments.pop(); switch( segment ) { case ".": break; case "..": if ((path.length === 0) || (path[path.length - 1] === "..")) { path.push( segment ); } else { path.pop(); } break; default: path.push( segment ); break; } } return path.join("/"); }; //todo - do the same for the toCSS env //tree.toCSSEnv = function (options) { //}; var copyFromOriginal = function(original, destination, propertiesToCopy) { if (!original) { return; } for(var i = 0; i < propertiesToCopy.length; i++) { if (original.hasOwnProperty(propertiesToCopy[i])) { destination[propertiesToCopy[i]] = original[propertiesToCopy[i]]; } } }; })(require('./tree')); (function (tree) { var _visitArgs = { visitDeeper: true }, _hasIndexed = false; function _noop(node) { return node; } function indexNodeTypes(parent, ticker) { // add .typeIndex to tree node types for lookup table var key, child; for (key in parent) { if (parent.hasOwnProperty(key)) { child = parent[key]; switch (typeof child) { case "function": // ignore bound functions directly on tree which do not have a prototype // or aren't nodes if (child.prototype && child.prototype.type) { child.prototype.typeIndex = ticker++; } break; case "object": ticker = indexNodeTypes(child, ticker); break; } } } return ticker; } tree.visitor = function(implementation) { this._implementation = implementation; this._visitFnCache = []; if (!_hasIndexed) { indexNodeTypes(tree, 1); _hasIndexed = true; } }; tree.visitor.prototype = { visit: function(node) { if (!node) { return node; } var nodeTypeIndex = node.typeIndex; if (!nodeTypeIndex) { return node; } var visitFnCache = this._visitFnCache, impl = this._implementation, aryIndx = nodeTypeIndex << 1, outAryIndex = aryIndx | 1, func = visitFnCache[aryIndx], funcOut = visitFnCache[outAryIndex], visitArgs = _visitArgs, fnName; visitArgs.visitDeeper = true; if (!func) { fnName = "visit" + node.type; func = impl[fnName] || _noop; funcOut = impl[fnName + "Out"] || _noop; visitFnCache[aryIndx] = func; visitFnCache[outAryIndex] = funcOut; } if (func !== _noop) { var newNode = func.call(impl, node, visitArgs); if (impl.isReplacing) { node = newNode; } } if (visitArgs.visitDeeper && node && node.accept) { node.accept(this); } if (funcOut != _noop) { funcOut.call(impl, node); } return node; }, visitArray: function(nodes, nonReplacing) { if (!nodes) { return nodes; } var cnt = nodes.length, i; // Non-replacing if (nonReplacing || !this._implementation.isReplacing) { for (i = 0; i < cnt; i++) { this.visit(nodes[i]); } return nodes; } // Replacing var out = []; for (i = 0; i < cnt; i++) { var evald = this.visit(nodes[i]); if (!evald.splice) { out.push(evald); } else if (evald.length) { this.flatten(evald, out); } } return out; }, flatten: function(arr, out) { if (!out) { out = []; } var cnt, i, item, nestedCnt, j, nestedItem; for (i = 0, cnt = arr.length; i < cnt; i++) { item = arr[i]; if (!item.splice) { out.push(item); continue; } for (j = 0, nestedCnt = item.length; j < nestedCnt; j++) { nestedItem = item[j]; if (!nestedItem.splice) { out.push(nestedItem); } else if (nestedItem.length) { this.flatten(nestedItem, out); } } } return out; } }; })(require('./tree')); (function (tree) { tree.importVisitor = function(importer, finish, evalEnv, onceFileDetectionMap, recursionDetector) { this._visitor = new tree.visitor(this); this._importer = importer; this._finish = finish; this.env = evalEnv || new tree.evalEnv(); this.importCount = 0; this.onceFileDetectionMap = onceFileDetectionMap || {}; this.recursionDetector = {}; if (recursionDetector) { for(var fullFilename in recursionDetector) { if (recursionDetector.hasOwnProperty(fullFilename)) { this.recursionDetector[fullFilename] = true; } } } }; tree.importVisitor.prototype = { isReplacing: true, run: function (root) { var error; try { // process the contents this._visitor.visit(root); } catch(e) { error = e; } this.isFinished = true; if (this.importCount === 0) { this._finish(error); } }, visitImport: function (importNode, visitArgs) { var importVisitor = this, evaldImportNode, inlineCSS = importNode.options.inline; if (!importNode.css || inlineCSS) { try { evaldImportNode = importNode.evalForImport(this.env); } catch(e){ if (!e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } // attempt to eval properly and treat as css importNode.css = true; // if that fails, this error will be thrown importNode.error = e; } if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) { importNode = evaldImportNode; this.importCount++; var env = new tree.evalEnv(this.env, this.env.frames.slice(0)); if (importNode.options.multiple) { env.importMultiple = true; } this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, importedAtRoot, fullPath) { if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } if (!env.importMultiple) { if (importedAtRoot) { importNode.skip = true; } else { importNode.skip = function() { if (fullPath in importVisitor.onceFileDetectionMap) { return true; } importVisitor.onceFileDetectionMap[fullPath] = true; return false; }; } } var subFinish = function(e) { importVisitor.importCount--; if (importVisitor.importCount === 0 && importVisitor.isFinished) { importVisitor._finish(e); } }; if (root) { importNode.root = root; importNode.importedFilename = fullPath; var duplicateImport = importedAtRoot || fullPath in importVisitor.recursionDetector; if (!inlineCSS && (env.importMultiple || !duplicateImport)) { importVisitor.recursionDetector[fullPath] = true; new(tree.importVisitor)(importVisitor._importer, subFinish, env, importVisitor.onceFileDetectionMap, importVisitor.recursionDetector) .run(root); return; } } subFinish(); }); } } visitArgs.visitDeeper = false; return importNode; }, visitRule: function (ruleNode, visitArgs) { visitArgs.visitDeeper = false; return ruleNode; }, visitDirective: function (directiveNode, visitArgs) { this.env.frames.unshift(directiveNode); return directiveNode; }, visitDirectiveOut: function (directiveNode) { this.env.frames.shift(); }, visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { this.env.frames.unshift(mixinDefinitionNode); return mixinDefinitionNode; }, visitMixinDefinitionOut: function (mixinDefinitionNode) { this.env.frames.shift(); }, visitRuleset: function (rulesetNode, visitArgs) { this.env.frames.unshift(rulesetNode); return rulesetNode; }, visitRulesetOut: function (rulesetNode) { this.env.frames.shift(); }, visitMedia: function (mediaNode, visitArgs) { this.env.frames.unshift(mediaNode.ruleset); return mediaNode; }, visitMediaOut: function (mediaNode) { this.env.frames.shift(); } }; })(require('./tree')); (function (tree) { tree.joinSelectorVisitor = function() { this.contexts = [[]]; this._visitor = new tree.visitor(this); }; tree.joinSelectorVisitor.prototype = { run: function (root) { return this._visitor.visit(root); }, visitRule: function (ruleNode, visitArgs) { visitArgs.visitDeeper = false; }, visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { visitArgs.visitDeeper = false; }, visitRuleset: function (rulesetNode, visitArgs) { var context = this.contexts[this.contexts.length - 1], paths = [], selectors; this.contexts.push(paths); if (! rulesetNode.root) { selectors = rulesetNode.selectors; if (selectors) { selectors = selectors.filter(function(selector) { return selector.getIsOutput(); }); rulesetNode.selectors = selectors.length ? selectors : (selectors = null); if (selectors) { rulesetNode.joinSelectors(paths, context, selectors); } } if (!selectors) { rulesetNode.rules = null; } rulesetNode.paths = paths; } }, visitRulesetOut: function (rulesetNode) { this.contexts.length = this.contexts.length - 1; }, visitMedia: function (mediaNode, visitArgs) { var context = this.contexts[this.contexts.length - 1]; mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia); } }; })(require('./tree')); (function (tree) { tree.toCSSVisitor = function(env) { this._visitor = new tree.visitor(this); this._env = env; }; tree.toCSSVisitor.prototype = { isReplacing: true, run: function (root) { return this._visitor.visit(root); }, visitRule: function (ruleNode, visitArgs) { if (ruleNode.variable) { return []; } return ruleNode; }, visitMixinDefinition: function (mixinNode, visitArgs) { // mixin definitions do not get eval'd - this means they keep state // so we have to clear that state here so it isn't used if toCSS is called twice mixinNode.frames = []; return []; }, visitExtend: function (extendNode, visitArgs) { return []; }, visitComment: function (commentNode, visitArgs) { if (commentNode.isSilent(this._env)) { return []; } return commentNode; }, visitMedia: function(mediaNode, visitArgs) { mediaNode.accept(this._visitor); visitArgs.visitDeeper = false; if (!mediaNode.rules.length) { return []; } return mediaNode; }, visitDirective: function(directiveNode, visitArgs) { if (directiveNode.currentFileInfo.reference && !directiveNode.isReferenced) { return []; } if (directiveNode.name === "@charset") { // Only output the debug info together with subsequent @charset definitions // a comment (or @media statement) before the actual @charset directive would // be considered illegal css as it has to be on the first line if (this.charset) { if (directiveNode.debugInfo) { var comment = new tree.Comment("/* " + directiveNode.toCSS(this._env).replace(/\n/g, "")+" */\n"); comment.debugInfo = directiveNode.debugInfo; return this._visitor.visit(comment); } return []; } this.charset = true; } return directiveNode; }, checkPropertiesInRoot: function(rules) { var ruleNode; for(var i = 0; i < rules.length; i++) { ruleNode = rules[i]; if (ruleNode instanceof tree.Rule && !ruleNode.variable) { throw { message: "properties must be inside selector blocks, they cannot be in the root.", index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null}; } } }, visitRuleset: function (rulesetNode, visitArgs) { var rule, rulesets = []; if (rulesetNode.firstRoot) { this.checkPropertiesInRoot(rulesetNode.rules); } if (! rulesetNode.root) { if (rulesetNode.paths) { rulesetNode.paths = rulesetNode.paths .filter(function(p) { var i; if (p[0].elements[0].combinator.value === ' ') { p[0].elements[0].combinator = new(tree.Combinator)(''); } for(i = 0; i < p.length; i++) { if (p[i].getIsReferenced() && p[i].getIsOutput()) { return true; } } return false; }); } // Compile rules and rulesets var nodeRules = rulesetNode.rules, nodeRuleCnt = nodeRules ? nodeRules.length : 0; for (var i = 0; i < nodeRuleCnt; ) { rule = nodeRules[i]; if (rule && rule.rules) { // visit because we are moving them out from being a child rulesets.push(this._visitor.visit(rule)); nodeRules.splice(i, 1); nodeRuleCnt--; continue; } i++; } // accept the visitor to remove rules and refactor itself // then we can decide now whether we want it or not if (nodeRuleCnt > 0) { rulesetNode.accept(this._visitor); } else { rulesetNode.rules = null; } visitArgs.visitDeeper = false; nodeRules = rulesetNode.rules; if (nodeRules) { this._mergeRules(nodeRules); nodeRules = rulesetNode.rules; } if (nodeRules) { this._removeDuplicateRules(nodeRules); nodeRules = rulesetNode.rules; } // now decide whether we keep the ruleset if (nodeRules && nodeRules.length > 0 && rulesetNode.paths.length > 0) { rulesets.splice(0, 0, rulesetNode); } } else { rulesetNode.accept(this._visitor); visitArgs.visitDeeper = false; if (rulesetNode.firstRoot || (rulesetNode.rules && rulesetNode.rules.length > 0)) { rulesets.splice(0, 0, rulesetNode); } } if (rulesets.length === 1) { return rulesets[0]; } return rulesets; }, _removeDuplicateRules: function(rules) { if (!rules) { return; } // remove duplicates var ruleCache = {}, ruleList, rule, i; for(i = rules.length - 1; i >= 0 ; i--) { rule = rules[i]; if (rule instanceof tree.Rule) { if (!ruleCache[rule.name]) { ruleCache[rule.name] = rule; } else { ruleList = ruleCache[rule.name]; if (ruleList instanceof tree.Rule) { ruleList = ruleCache[rule.name] = [ruleCache[rule.name].toCSS(this._env)]; } var ruleCSS = rule.toCSS(this._env); if (ruleList.indexOf(ruleCSS) !== -1) { rules.splice(i, 1); } else { ruleList.push(ruleCSS); } } } } }, _mergeRules: function (rules) { if (!rules) { return; } var groups = {}, parts, rule, key; for (var i = 0; i < rules.length; i++) { rule = rules[i]; if ((rule instanceof tree.Rule) && rule.merge) { key = [rule.name, rule.important ? "!" : ""].join(","); if (!groups[key]) { groups[key] = []; } else { rules.splice(i--, 1); } groups[key].push(rule); } } Object.keys(groups).map(function (k) { function toExpression(values) { return new (tree.Expression)(values.map(function (p) { return p.value; })); } function toValue(values) { return new (tree.Value)(values.map(function (p) { return p; })); } parts = groups[k]; if (parts.length > 1) { rule = parts[0]; var spacedGroups = []; var lastSpacedGroup = []; parts.map(function (p) { if (p.merge==="+") { if (lastSpacedGroup.length > 0) { spacedGroups.push(toExpression(lastSpacedGroup)); } lastSpacedGroup = []; } lastSpacedGroup.push(p); }); spacedGroups.push(toExpression(lastSpacedGroup)); rule.value = toValue(spacedGroups); } }); } }; })(require('./tree')); (function (tree) { /*jshint loopfunc:true */ tree.extendFinderVisitor = function() { this._visitor = new tree.visitor(this); this.contexts = []; this.allExtendsStack = [[]]; }; tree.extendFinderVisitor.prototype = { run: function (root) { root = this._visitor.visit(root); root.allExtends = this.allExtendsStack[0]; return root; }, visitRule: function (ruleNode, visitArgs) { visitArgs.visitDeeper = false; }, visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { visitArgs.visitDeeper = false; }, visitRuleset: function (rulesetNode, visitArgs) { if (rulesetNode.root) { return; } var i, j, extend, allSelectorsExtendList = [], extendList; // get &:extend(.a); rules which apply to all selectors in this ruleset var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0; for(i = 0; i < ruleCnt; i++) { if (rulesetNode.rules[i] instanceof tree.Extend) { allSelectorsExtendList.push(rules[i]); rulesetNode.extendOnEveryPath = true; } } // now find every selector and apply the extends that apply to all extends // and the ones which apply to an individual extend var paths = rulesetNode.paths; for(i = 0; i < paths.length; i++) { var selectorPath = paths[i], selector = selectorPath[selectorPath.length - 1], selExtendList = selector.extendList; extendList = selExtendList ? selExtendList.slice(0).concat(allSelectorsExtendList) : allSelectorsExtendList; if (extendList) { extendList = extendList.map(function(allSelectorsExtend) { return allSelectorsExtend.clone(); }); } for(j = 0; j < extendList.length; j++) { this.foundExtends = true; extend = extendList[j]; extend.findSelfSelectors(selectorPath); extend.ruleset = rulesetNode; if (j === 0) { extend.firstExtendOnThisSelectorPath = true; } this.allExtendsStack[this.allExtendsStack.length-1].push(extend); } } this.contexts.push(rulesetNode.selectors); }, visitRulesetOut: function (rulesetNode) { if (!rulesetNode.root) { this.contexts.length = this.contexts.length - 1; } }, visitMedia: function (mediaNode, visitArgs) { mediaNode.allExtends = []; this.allExtendsStack.push(mediaNode.allExtends); }, visitMediaOut: function (mediaNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; }, visitDirective: function (directiveNode, visitArgs) { directiveNode.allExtends = []; this.allExtendsStack.push(directiveNode.allExtends); }, visitDirectiveOut: function (directiveNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; } }; tree.processExtendsVisitor = function() { this._visitor = new tree.visitor(this); }; tree.processExtendsVisitor.prototype = { run: function(root) { var extendFinder = new tree.extendFinderVisitor(); extendFinder.run(root); if (!extendFinder.foundExtends) { return root; } root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends)); this.allExtendsStack = [root.allExtends]; return this._visitor.visit(root); }, doExtendChaining: function (extendsList, extendsListTarget, iterationCount) { // // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting // the selector we would do normally, but we are also adding an extend with the same target selector // this means this new extend can then go and alter other extends // // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if // we look at each selector at a time, as is done in visitRuleset var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend, newExtend; iterationCount = iterationCount || 0; //loop through comparing every extend with every target extend. // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one // and the second is the target. // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the // case when processing media queries for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){ for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){ extend = extendsList[extendIndex]; targetExtend = extendsListTarget[targetExtendIndex]; // look for circular references if( extend.parent_ids.indexOf( targetExtend.object_id ) >= 0 ){ continue; } // find a match in the target extends self selector (the bit before :extend) selectorPath = [targetExtend.selfSelectors[0]]; matches = extendVisitor.findMatch(extend, selectorPath); if (matches.length) { // we found a match, so for each self selector.. extend.selfSelectors.forEach(function(selfSelector) { // process the extend as usual newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector); // but now we create a new extend from it newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0); newExtend.selfSelectors = newSelector; // add the extend onto the list of extends for that selector newSelector[newSelector.length-1].extendList = [newExtend]; // record that we need to add it. extendsToAdd.push(newExtend); newExtend.ruleset = targetExtend.ruleset; //remember its parents for circular references newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids); // only process the selector once.. if we have :extend(.a,.b) then multiple // extends will look at the same selector path, so when extending // we know that any others will be duplicates in terms of what is added to the css if (targetExtend.firstExtendOnThisSelectorPath) { newExtend.firstExtendOnThisSelectorPath = true; targetExtend.ruleset.paths.push(newSelector); } }); } } } if (extendsToAdd.length) { // try to detect circular references to stop a stack overflow. // may no longer be needed. this.extendChainCount++; if (iterationCount > 100) { var selectorOne = "{unable to calculate}"; var selectorTwo = "{unable to calculate}"; try { selectorOne = extendsToAdd[0].selfSelectors[0].toCSS(); selectorTwo = extendsToAdd[0].selector.toCSS(); } catch(e) {} throw {message: "extend circular reference detected. One of the circular extends is currently:"+selectorOne+":extend(" + selectorTwo+")"}; } // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1)); } else { return extendsToAdd; } }, visitRule: function (ruleNode, visitArgs) { visitArgs.visitDeeper = false; }, visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { visitArgs.visitDeeper = false; }, visitSelector: function (selectorNode, visitArgs) { visitArgs.visitDeeper = false; }, visitRuleset: function (rulesetNode, visitArgs) { if (rulesetNode.root) { return; } var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this, selectorPath; // look at each selector path in the ruleset, find any extend matches and then copy, find and replace for(extendIndex = 0; extendIndex < allExtends.length; extendIndex++) { for(pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) { selectorPath = rulesetNode.paths[pathIndex]; // extending extends happens initially, before the main pass if (rulesetNode.extendOnEveryPath) { continue; } var extendList = selectorPath[selectorPath.length-1].extendList; if (extendList && extendList.length) { continue; } matches = this.findMatch(allExtends[extendIndex], selectorPath); if (matches.length) { allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) { selectorsToAdd.push(extendVisitor.extendSelector(matches, selectorPath, selfSelector)); }); } } } rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd); }, findMatch: function (extend, haystackSelectorPath) { // // look through the haystack selector path to try and find the needle - extend.selector // returns an array of selector matches that can then be replaced // var haystackSelectorIndex, hackstackSelector, hackstackElementIndex, haystackElement, targetCombinator, i, extendVisitor = this, needleElements = extend.selector.elements, potentialMatches = [], potentialMatch, matches = []; // loop through the haystack elements for(haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) { hackstackSelector = haystackSelectorPath[haystackSelectorIndex]; for(hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) { haystackElement = hackstackSelector.elements[hackstackElementIndex]; // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) { potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator}); } for(i = 0; i < potentialMatches.length; i++) { potentialMatch = potentialMatches[i]; // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out // what the resulting combinator will be targetCombinator = haystackElement.combinator.value; if (targetCombinator === '' && hackstackElementIndex === 0) { targetCombinator = ' '; } // if we don't match, null our match to indicate failure if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) || (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) { potentialMatch = null; } else { potentialMatch.matched++; } // if we are still valid and have finished, test whether we have elements after and whether these are allowed if (potentialMatch) { potentialMatch.finished = potentialMatch.matched === needleElements.length; if (potentialMatch.finished && (!extend.allowAfter && (hackstackElementIndex+1 < hackstackSelector.elements.length || haystackSelectorIndex+1 < haystackSelectorPath.length))) { potentialMatch = null; } } // if null we remove, if not, we are still valid, so either push as a valid match or continue if (potentialMatch) { if (potentialMatch.finished) { potentialMatch.length = needleElements.length; potentialMatch.endPathIndex = haystackSelectorIndex; potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again matches.push(potentialMatch); } } else { potentialMatches.splice(i, 1); i--; } } } } return matches; }, isElementValuesEqual: function(elementValue1, elementValue2) { if (typeof elementValue1 === "string" || typeof elementValue2 === "string") { return elementValue1 === elementValue2; } if (elementValue1 instanceof tree.Attribute) { if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) { return false; } if (!elementValue1.value || !elementValue2.value) { if (elementValue1.value || elementValue2.value) { return false; } return true; } elementValue1 = elementValue1.value.value || elementValue1.value; elementValue2 = elementValue2.value.value || elementValue2.value; return elementValue1 === elementValue2; } elementValue1 = elementValue1.value; elementValue2 = elementValue2.value; if (elementValue1 instanceof tree.Selector) { if (!(elementValue2 instanceof tree.Selector) || elementValue1.elements.length !== elementValue2.elements.length) { return false; } for(var i = 0; i currentSelectorPathIndex && currentSelectorPathElementIndex > 0) { path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); currentSelectorPathElementIndex = 0; currentSelectorPathIndex++; } newElements = selector.elements .slice(currentSelectorPathElementIndex, match.index) .concat([firstElement]) .concat(replacementSelector.elements.slice(1)); if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) { path[path.length - 1].elements = path[path.length - 1].elements.concat(newElements); } else { path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); path.push(new tree.Selector( newElements )); } currentSelectorPathIndex = match.endPathIndex; currentSelectorPathElementIndex = match.endPathElementIndex; if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) { currentSelectorPathElementIndex = 0; currentSelectorPathIndex++; } } if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) { path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); currentSelectorPathIndex++; } path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length)); return path; }, visitRulesetOut: function (rulesetNode) { }, visitMedia: function (mediaNode, visitArgs) { var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends)); this.allExtendsStack.push(newAllExtends); }, visitMediaOut: function (mediaNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; }, visitDirective: function (directiveNode, visitArgs) { var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends)); this.allExtendsStack.push(newAllExtends); }, visitDirectiveOut: function (directiveNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; } }; })(require('./tree')); (function (tree) { tree.sourceMapOutput = function (options) { this._css = []; this._rootNode = options.rootNode; this._writeSourceMap = options.writeSourceMap; this._contentsMap = options.contentsMap; this._contentsIgnoredCharsMap = options.contentsIgnoredCharsMap; this._sourceMapFilename = options.sourceMapFilename; this._outputFilename = options.outputFilename; this._sourceMapURL = options.sourceMapURL; if (options.sourceMapBasepath) { this._sourceMapBasepath = options.sourceMapBasepath.replace(/\\/g, '/'); } this._sourceMapRootpath = options.sourceMapRootpath; this._outputSourceFiles = options.outputSourceFiles; this._sourceMapGeneratorConstructor = options.sourceMapGenerator || require("source-map").SourceMapGenerator; if (this._sourceMapRootpath && this._sourceMapRootpath.charAt(this._sourceMapRootpath.length-1) !== '/') { this._sourceMapRootpath += '/'; } this._lineNumber = 0; this._column = 0; }; tree.sourceMapOutput.prototype.normalizeFilename = function(filename) { filename = filename.replace(/\\/g, '/'); if (this._sourceMapBasepath && filename.indexOf(this._sourceMapBasepath) === 0) { filename = filename.substring(this._sourceMapBasepath.length); if (filename.charAt(0) === '\\' || filename.charAt(0) === '/') { filename = filename.substring(1); } } return (this._sourceMapRootpath || "") + filename; }; tree.sourceMapOutput.prototype.add = function(chunk, fileInfo, index, mapLines) { //ignore adding empty strings if (!chunk) { return; } var lines, sourceLines, columns, sourceColumns, i; if (fileInfo) { var inputSource = this._contentsMap[fileInfo.filename]; // remove vars/banner added to the top of the file if (this._contentsIgnoredCharsMap[fileInfo.filename]) { // adjust the index index -= this._contentsIgnoredCharsMap[fileInfo.filename]; if (index < 0) { index = 0; } // adjust the source inputSource = inputSource.slice(this._contentsIgnoredCharsMap[fileInfo.filename]); } inputSource = inputSource.substring(0, index); sourceLines = inputSource.split("\n"); sourceColumns = sourceLines[sourceLines.length-1]; } lines = chunk.split("\n"); columns = lines[lines.length-1]; if (fileInfo) { if (!mapLines) { this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + 1, column: this._column}, original: { line: sourceLines.length, column: sourceColumns.length}, source: this.normalizeFilename(fileInfo.filename)}); } else { for(i = 0; i < lines.length; i++) { this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + i + 1, column: i === 0 ? this._column : 0}, original: { line: sourceLines.length + i, column: i === 0 ? sourceColumns.length : 0}, source: this.normalizeFilename(fileInfo.filename)}); } } } if (lines.length === 1) { this._column += columns.length; } else { this._lineNumber += lines.length - 1; this._column = columns.length; } this._css.push(chunk); }; tree.sourceMapOutput.prototype.isEmpty = function() { return this._css.length === 0; }; tree.sourceMapOutput.prototype.toCSS = function(env) { this._sourceMapGenerator = new this._sourceMapGeneratorConstructor({ file: this._outputFilename, sourceRoot: null }); if (this._outputSourceFiles) { for(var filename in this._contentsMap) { if (this._contentsMap.hasOwnProperty(filename)) { var source = this._contentsMap[filename]; if (this._contentsIgnoredCharsMap[filename]) { source = source.slice(this._contentsIgnoredCharsMap[filename]); } this._sourceMapGenerator.setSourceContent(this.normalizeFilename(filename), source); } } } this._rootNode.genCSS(env, this); if (this._css.length > 0) { var sourceMapURL, sourceMapContent = JSON.stringify(this._sourceMapGenerator.toJSON()); if (this._sourceMapURL) { sourceMapURL = this._sourceMapURL; } else if (this._sourceMapFilename) { sourceMapURL = this.normalizeFilename(this._sourceMapFilename); } if (this._writeSourceMap) { this._writeSourceMap(sourceMapContent); } else { sourceMapURL = "data:application/json," + encodeURIComponent(sourceMapContent); } if (sourceMapURL) { this._css.push("/*# sourceMappingURL=" + sourceMapURL + " */"); } } return this._css.join(''); }; })(require('./tree')); // // browser.js - client-side engine // /*global less, window, document, XMLHttpRequest, location */ var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol); less.env = less.env || (location.hostname == '127.0.0.1' || location.hostname == '0.0.0.0' || location.hostname == 'localhost' || (location.port && location.port.length > 0) || isFileProtocol ? 'development' : 'production'); var logLevel = { debug: 3, info: 2, errors: 1, none: 0 }; // The amount of logging in the javascript console. // 3 - Debug, information and errors // 2 - Information and errors // 1 - Errors // 0 - None // Defaults to 2 less.logLevel = typeof(less.logLevel) != 'undefined' ? less.logLevel : (less.env === 'development' ? logLevel.debug : logLevel.errors); // Load styles asynchronously (default: false) // // This is set to `false` by default, so that the body // doesn't start loading before the stylesheets are parsed. // Setting this to `true` can result in flickering. // less.async = less.async || false; less.fileAsync = less.fileAsync || false; // Interval between watch polls less.poll = less.poll || (isFileProtocol ? 1000 : 1500); //Setup user functions if (less.functions) { for(var func in less.functions) { if (less.functions.hasOwnProperty(func)) { less.tree.functions[func] = less.functions[func]; } } } var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash); if (dumpLineNumbers) { less.dumpLineNumbers = dumpLineNumbers[1]; } var typePattern = /^text\/(x-)?less$/; var cache = null; var fileCache = {}; function log(str, level) { if (typeof(console) !== 'undefined' && less.logLevel >= level) { console.log('less: ' + str); } } function extractId(href) { return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' ) // Remove protocol & domain .replace(/^\//, '' ) // Remove root / .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension .replace(/[^\.\w-]+/g, '-') // Replace illegal characters .replace(/\./g, ':'); // Replace dots with colons(for valid id) } function errorConsole(e, rootHref) { var template = '{line} {content}'; var filename = e.filename || rootHref; var errors = []; var content = (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + " in " + filename + " "; var errorline = function (e, i, classname) { if (e.extract[i] !== undefined) { errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1)) .replace(/\{class\}/, classname) .replace(/\{content\}/, e.extract[i])); } }; if (e.extract) { errorline(e, 0, ''); errorline(e, 1, 'line'); errorline(e, 2, ''); content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n' + errors.join('\n'); } else if (e.stack) { content += e.stack; } log(content, logLevel.errors); } function createCSS(styles, sheet, lastModified) { // Strip the query-string var href = sheet.href || ''; // If there is no title set, use the filename, minus the extension var id = 'less:' + (sheet.title || extractId(href)); // If this has already been inserted into the DOM, we may need to replace it var oldCss = document.getElementById(id); var keepOldCss = false; // Create a new stylesheet node for insertion or (if necessary) replacement var css = document.createElement('style'); css.setAttribute('type', 'text/css'); if (sheet.media) { css.setAttribute('media', sheet.media); } css.id = id; if (css.styleSheet) { // IE try { css.styleSheet.cssText = styles; } catch (e) { throw new(Error)("Couldn't reassign styleSheet.cssText."); } } else { css.appendChild(document.createTextNode(styles)); // If new contents match contents of oldCss, don't replace oldCss keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 && oldCss.firstChild.nodeValue === css.firstChild.nodeValue); } var head = document.getElementsByTagName('head')[0]; // If there is no oldCss, just append; otherwise, only append if we need // to replace oldCss with an updated stylesheet if (oldCss === null || keepOldCss === false) { var nextEl = sheet && sheet.nextSibling || null; if (nextEl) { nextEl.parentNode.insertBefore(css, nextEl); } else { head.appendChild(css); } } if (oldCss && keepOldCss === false) { oldCss.parentNode.removeChild(oldCss); } // Don't update the local store if the file wasn't modified if (lastModified && cache) { log('saving ' + href + ' to cache.', logLevel.info); try { cache.setItem(href, styles); cache.setItem(href + ':timestamp', lastModified); } catch(e) { //TODO - could do with adding more robust error handling log('failed to save', logLevel.errors); } } } function postProcessCSS(styles) { if (less.postProcessor && typeof less.postProcessor === 'function') { styles = less.postProcessor.call(styles, styles) || styles; } return styles; } function errorHTML(e, rootHref) { var id = 'less-error-message:' + extractId(rootHref || ""); var template = '
  • {content}
  • '; var elem = document.createElement('div'), timer, content, errors = []; var filename = e.filename || rootHref; var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; elem.id = id; elem.className = "less-error-message"; content = '

    ' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + '

    ' + '

    in ' + filenameNoPath + " "; var errorline = function (e, i, classname) { if (e.extract[i] !== undefined) { errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1)) .replace(/\{class\}/, classname) .replace(/\{content\}/, e.extract[i])); } }; if (e.extract) { errorline(e, 0, ''); errorline(e, 1, 'line'); errorline(e, 2, ''); content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + '
      ' + errors.join('') + '
    '; } else if (e.stack) { content += '
    ' + e.stack.split('\n').slice(1).join('
    '); } elem.innerHTML = content; // CSS for error messages createCSS([ '.less-error-message ul, .less-error-message li {', 'list-style-type: none;', 'margin-right: 15px;', 'padding: 4px 0;', 'margin: 0;', '}', '.less-error-message label {', 'font-size: 12px;', 'margin-right: 15px;', 'padding: 4px 0;', 'color: #cc7777;', '}', '.less-error-message pre {', 'color: #dd6666;', 'padding: 4px 0;', 'margin: 0;', 'display: inline-block;', '}', '.less-error-message pre.line {', 'color: #ff0000;', '}', '.less-error-message h3 {', 'font-size: 20px;', 'font-weight: bold;', 'padding: 15px 0 5px 0;', 'margin: 0;', '}', '.less-error-message a {', 'color: #10a', '}', '.less-error-message .error {', 'color: red;', 'font-weight: bold;', 'padding-bottom: 2px;', 'border-bottom: 1px dashed red;', '}' ].join('\n'), { title: 'error-message' }); elem.style.cssText = [ "font-family: Arial, sans-serif", "border: 1px solid #e00", "background-color: #eee", "border-radius: 5px", "-webkit-border-radius: 5px", "-moz-border-radius: 5px", "color: #e00", "padding: 15px", "margin-bottom: 15px" ].join(';'); if (less.env == 'development') { timer = setInterval(function () { if (document.body) { if (document.getElementById(id)) { document.body.replaceChild(elem, document.getElementById(id)); } else { document.body.insertBefore(elem, document.body.firstChild); } clearInterval(timer); } }, 10); } } function error(e, rootHref) { if (!less.errorReporting || less.errorReporting === "html") { errorHTML(e, rootHref); } else if (less.errorReporting === "console") { errorConsole(e, rootHref); } else if (typeof less.errorReporting === 'function') { less.errorReporting("add", e, rootHref); } } function removeErrorHTML(path) { var node = document.getElementById('less-error-message:' + extractId(path)); if (node) { node.parentNode.removeChild(node); } } function removeErrorConsole(path) { //no action } function removeError(path) { if (!less.errorReporting || less.errorReporting === "html") { removeErrorHTML(path); } else if (less.errorReporting === "console") { removeErrorConsole(path); } else if (typeof less.errorReporting === 'function') { less.errorReporting("remove", path); } } function loadStyles(modifyVars) { var styles = document.getElementsByTagName('style'), style; for (var i = 0; i < styles.length; i++) { style = styles[i]; if (style.type.match(typePattern)) { var env = new less.tree.parseEnv(less), lessText = style.innerHTML || ''; env.filename = document.location.href.replace(/#.*$/, ''); if (modifyVars || less.globalVars) { env.useFileCache = true; } /*jshint loopfunc:true */ // use closure to store current value of i var callback = (function(style) { return function (e, cssAST) { if (e) { return error(e, "inline"); } var css = cssAST.toCSS(less); style.type = 'text/css'; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.innerHTML = css; } }; })(style); new(less.Parser)(env).parse(lessText, callback, {globalVars: less.globalVars, modifyVars: modifyVars}); } } } function extractUrlParts(url, baseUrl) { // urlParts[1] = protocol&hostname || / // urlParts[2] = / if path relative to host base // urlParts[3] = directories // urlParts[4] = filename // urlParts[5] = parameters var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i, urlParts = url.match(urlPartsRegex), returner = {}, directories = [], i, baseUrlParts; if (!urlParts) { throw new Error("Could not parse sheet href - '"+url+"'"); } // Stylesheets in IE don't always return the full path if (!urlParts[1] || urlParts[2]) { baseUrlParts = baseUrl.match(urlPartsRegex); if (!baseUrlParts) { throw new Error("Could not parse page url - '"+baseUrl+"'"); } urlParts[1] = urlParts[1] || baseUrlParts[1] || ""; if (!urlParts[2]) { urlParts[3] = baseUrlParts[3] + urlParts[3]; } } if (urlParts[3]) { directories = urlParts[3].replace(/\\/g, "/").split("/"); // extract out . before .. so .. doesn't absorb a non-directory for(i = 0; i < directories.length; i++) { if (directories[i] === ".") { directories.splice(i, 1); i -= 1; } } for(i = 0; i < directories.length; i++) { if (directories[i] === ".." && i > 0) { directories.splice(i-1, 2); i -= 2; } } } returner.hostPart = urlParts[1]; returner.directories = directories; returner.path = urlParts[1] + directories.join("/"); returner.fileUrl = returner.path + (urlParts[4] || ""); returner.url = returner.fileUrl + (urlParts[5] || ""); return returner; } function pathDiff(url, baseUrl) { // diff between two paths to create a relative path var urlParts = extractUrlParts(url), baseUrlParts = extractUrlParts(baseUrl), i, max, urlDirectories, baseUrlDirectories, diff = ""; if (urlParts.hostPart !== baseUrlParts.hostPart) { return ""; } max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); for(i = 0; i < max; i++) { if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } } baseUrlDirectories = baseUrlParts.directories.slice(i); urlDirectories = urlParts.directories.slice(i); for(i = 0; i < baseUrlDirectories.length-1; i++) { diff += "../"; } for(i = 0; i < urlDirectories.length-1; i++) { diff += urlDirectories[i] + "/"; } return diff; } function getXMLHttpRequest() { if (window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject)) { return new XMLHttpRequest(); } else { try { /*global ActiveXObject */ return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { log("browser doesn't support AJAX.", logLevel.errors); return null; } } } function doXHR(url, type, callback, errback) { var xhr = getXMLHttpRequest(); var async = isFileProtocol ? less.fileAsync : less.async; if (typeof(xhr.overrideMimeType) === 'function') { xhr.overrideMimeType('text/css'); } log("XHR: Getting '" + url + "'", logLevel.debug); xhr.open('GET', url, async); xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); xhr.send(null); function handleResponse(xhr, callback, errback) { if (xhr.status >= 200 && xhr.status < 300) { callback(xhr.responseText, xhr.getResponseHeader("Last-Modified")); } else if (typeof(errback) === 'function') { errback(xhr.status, url); } } if (isFileProtocol && !less.fileAsync) { if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { callback(xhr.responseText); } else { errback(xhr.status, url); } } else if (async) { xhr.onreadystatechange = function () { if (xhr.readyState == 4) { handleResponse(xhr, callback, errback); } }; } else { handleResponse(xhr, callback, errback); } } function loadFile(originalHref, currentFileInfo, callback, env, modifyVars) { if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\//.test(originalHref)) { originalHref = currentFileInfo.currentDirectory + originalHref; } // sheet may be set to the stylesheet for the initial load or a collection of properties including // some env variables for imports var hrefParts = extractUrlParts(originalHref, window.location.href); var href = hrefParts.url; var newFileInfo = { currentDirectory: hrefParts.path, filename: href }; if (currentFileInfo) { newFileInfo.entryPath = currentFileInfo.entryPath; newFileInfo.rootpath = currentFileInfo.rootpath; newFileInfo.rootFilename = currentFileInfo.rootFilename; newFileInfo.relativeUrls = currentFileInfo.relativeUrls; } else { newFileInfo.entryPath = hrefParts.path; newFileInfo.rootpath = less.rootpath || hrefParts.path; newFileInfo.rootFilename = href; newFileInfo.relativeUrls = env.relativeUrls; } if (newFileInfo.relativeUrls) { if (env.rootpath) { newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path; } else { newFileInfo.rootpath = hrefParts.path; } } if (env.useFileCache && fileCache[href]) { try { var lessText = fileCache[href]; callback(null, lessText, href, newFileInfo, { lastModified: new Date() }); } catch (e) { callback(e, null, href); } return; } doXHR(href, env.mime, function (data, lastModified) { // per file cache fileCache[href] = data; // Use remote copy (re-parse) try { callback(null, data, href, newFileInfo, { lastModified: lastModified }); } catch (e) { callback(e, null, href); } }, function (status, url) { callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, href); }); } function loadStyleSheet(sheet, callback, reload, remaining, modifyVars) { var env = new less.tree.parseEnv(less); env.mime = sheet.type; if (modifyVars || less.globalVars) { env.useFileCache = true; } loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) { if (webInfo) { webInfo.remaining = remaining; var css = cache && cache.getItem(path), timestamp = cache && cache.getItem(path + ':timestamp'); if (!reload && timestamp && webInfo.lastModified && (new(Date)(webInfo.lastModified).valueOf() === new(Date)(timestamp).valueOf())) { // Use local copy createCSS(css, sheet); webInfo.local = true; callback(null, null, data, sheet, webInfo, path); return; } } //TODO add tests around how this behaves when reloading removeError(path); if (data) { env.currentFileInfo = newFileInfo; new(less.Parser)(env).parse(data, function (e, root) { if (e) { return callback(e, null, null, sheet); } try { callback(e, root, data, sheet, webInfo, path); } catch (e) { callback(e, null, null, sheet); } }, {modifyVars: modifyVars, globalVars: less.globalVars}); } else { callback(e, null, null, sheet, webInfo, path); } }, env, modifyVars); } function loadStyleSheets(callback, reload, modifyVars) { for (var i = 0; i < less.sheets.length; i++) { loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), modifyVars); } } function initRunningMode(){ if (less.env === 'development') { less.optimization = 0; less.watchTimer = setInterval(function () { if (less.watchMode) { loadStyleSheets(function (e, root, _, sheet, env) { if (e) { error(e, sheet.href); } else if (root) { var styles = root.toCSS(less); styles = postProcessCSS(styles); createCSS(styles, sheet, env.lastModified); } }); } }, less.poll); } else { less.optimization = 3; } } // // Watch mode // less.watch = function () { if (!less.watchMode ){ less.env = 'development'; initRunningMode(); } this.watchMode = true; return true; }; less.unwatch = function () {clearInterval(less.watchTimer); this.watchMode = false; return false; }; if (/!watch/.test(location.hash)) { less.watch(); } if (less.env != 'development') { try { cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; } catch (_) {} } // // Get all tags with the 'rel' attribute set to "stylesheet/less" // var links = document.getElementsByTagName('link'); less.sheets = []; for (var i = 0; i < links.length; i++) { if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && (links[i].type.match(typePattern)))) { less.sheets.push(links[i]); } } // // With this function, it's possible to alter variables and re-render // CSS without reloading less-files // less.modifyVars = function(record) { less.refresh(false, record); }; less.refresh = function (reload, modifyVars) { var startTime, endTime; startTime = endTime = new Date(); loadStyleSheets(function (e, root, _, sheet, env) { if (e) { return error(e, sheet.href); } if (env.local) { log("loading " + sheet.href + " from cache.", logLevel.info); } else { log("parsed " + sheet.href + " successfully.", logLevel.debug); var styles = root.toCSS(less); styles = postProcessCSS(styles); createCSS(styles, sheet, env.lastModified); } log("css for " + sheet.href + " generated in " + (new Date() - endTime) + 'ms', logLevel.info); if (env.remaining === 0) { log("less has finished. css generated in " + (new Date() - startTime) + 'ms', logLevel.info); } endTime = new Date(); }, reload, modifyVars); loadStyles(modifyVars); }; less.refreshStyles = loadStyles; less.Parser.fileLoader = loadFile; less.refresh(less.env === 'development'); // amd.js // // Define Less as an AMD module. if (typeof define === "function" && define.amd) { define(function () { return less; } ); } })(window); ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/test.html ================================================ Api2Doc 接口测试

    XXX接口测试

    请求参数

    如果是 POST 方法,这些请求参数会放在 Body 中; 如果是其它方法(如:GET / PUT / DELETE / PATCH 等),则会放在 URL 的 Query 串中。
    +

    请求Header

    +
    发送

    返回结果

    
    {
      "requestId" : "a22c721867984258846686b89dbf82db",
      "serverTime" : 1524039750370,
      "spendTime" : 5,
      "resultCode" : "success"
    }
            

    输出日志

    
      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v1.5.9.RELEASE)
    
    2018-04-26 10:42:19.193  INFO 2328 --- [           main] c.terran4j.demo.api2doc.Api2DocDemoApp   : Starting Api2DocDemoApp on DESKTOP-32DF0L3 with PID 2328 (C:\Users\jiangwei\IdeaProjects\commons\commons-api2doc\target\test-classes started by jiangwei in C:\Users\jiangwei\IdeaProjects\commons)
    2018-04-26 10:42:19.200  INFO 2328 --- [           main] c.terran4j.demo.api2doc.Api2DocDemoApp   : No active profile set, falling back to default profiles: default
    2018-04-26 10:42:19.479  INFO 2328 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4a22f9e2: startup date [Thu Apr 26 10:42:19 CST 2018]; root of context hierarchy
    2018-04-26 10:42:24.188  INFO 2328 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'api2DocService' of type [com.terran4j.commons.api2doc.impl.Api2DocService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
    2018-04-26 10:42:26.506  INFO 2328 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
    2018-04-26 10:42:26.529  INFO 2328 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2018-04-26 10:42:26.531  INFO 2328 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
    2018-04-26 10:42:27.034  INFO 2328 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
            
    ================================================ FILE: commons-api2doc/src/main/resources/static/api2doc/vue/vue-2.5.10.js ================================================ /*! * Vue.js v2.5.10 * (c) 2014-2017 Evan You * Released under the MIT License. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Vue = factory()); }(this, (function () { 'use strict'; /* */ var emptyObject = Object.freeze({}); // these helpers produces better vm code in JS engines due to their // explicitness and function inlining function isUndef (v) { return v === undefined || v === null } function isDef (v) { return v !== undefined && v !== null } function isTrue (v) { return v === true } function isFalse (v) { return v === false } /** * Check if value is primitive */ function isPrimitive (value) { return ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ) } /** * Quick object check - this is primarily used to tell * Objects from primitive values when we know the value * is a JSON-compliant type. */ function isObject (obj) { return obj !== null && typeof obj === 'object' } /** * Get the raw type string of a value e.g. [object Object] */ var _toString = Object.prototype.toString; function toRawType (value) { return _toString.call(value).slice(8, -1) } /** * Strict object type check. Only returns true * for plain JavaScript objects. */ function isPlainObject (obj) { return _toString.call(obj) === '[object Object]' } function isRegExp (v) { return _toString.call(v) === '[object RegExp]' } /** * Check if val is a valid array index. */ function isValidArrayIndex (val) { var n = parseFloat(String(val)); return n >= 0 && Math.floor(n) === n && isFinite(val) } /** * Convert a value to a string that is actually rendered. */ function toString (val) { return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val) } /** * Convert a input value to a number for persistence. * If the conversion fails, return original string. */ function toNumber (val) { var n = parseFloat(val); return isNaN(n) ? val : n } /** * Make a map and return a function for checking if a key * is in that map. */ function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(','); for (var i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; } } /** * Check if a tag is a built-in tag. */ var isBuiltInTag = makeMap('slot,component', true); /** * Check if a attribute is a reserved attribute. */ var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); /** * Remove an item from an array */ function remove (arr, item) { if (arr.length) { var index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1) } } } /** * Check whether the object has the property. */ var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } /** * Create a cached version of a pure function. */ function cached (fn) { var cache = Object.create(null); return (function cachedFn (str) { var hit = cache[str]; return hit || (cache[str] = fn(str)) }) } /** * Camelize a hyphen-delimited string. */ var camelizeRE = /-(\w)/g; var camelize = cached(function (str) { return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) }); /** * Capitalize a string. */ var capitalize = cached(function (str) { return str.charAt(0).toUpperCase() + str.slice(1) }); /** * Hyphenate a camelCase string. */ var hyphenateRE = /\B([A-Z])/g; var hyphenate = cached(function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase() }); /** * Simple bind, faster than native */ function bind (fn, ctx) { function boundFn (a) { var l = arguments.length; return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } // record original fn length boundFn._length = fn.length; return boundFn } /** * Convert an Array-like object to a real Array. */ function toArray (list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret } /** * Mix properties into target object. */ function extend (to, _from) { for (var key in _from) { to[key] = _from[key]; } return to } /** * Merge an Array of Objects into a single Object. */ function toObject (arr) { var res = {}; for (var i = 0; i < arr.length; i++) { if (arr[i]) { extend(res, arr[i]); } } return res } /** * Perform no operation. * Stubbing args to make Flow happy without leaving useless transpiled code * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) */ function noop (a, b, c) {} /** * Always return false. */ var no = function (a, b, c) { return false; }; /** * Return same value */ var identity = function (_) { return _; }; /** * Generate a static keys string from compiler modules. */ function genStaticKeys (modules) { return modules.reduce(function (keys, m) { return keys.concat(m.staticKeys || []) }, []).join(',') } /** * Check if two values are loosely equal - that is, * if they are plain objects, do they have the same shape? */ function looseEqual (a, b) { if (a === b) { return true } var isObjectA = isObject(a); var isObjectB = isObject(b); if (isObjectA && isObjectB) { try { var isArrayA = Array.isArray(a); var isArrayB = Array.isArray(b); if (isArrayA && isArrayB) { return a.length === b.length && a.every(function (e, i) { return looseEqual(e, b[i]) }) } else if (!isArrayA && !isArrayB) { var keysA = Object.keys(a); var keysB = Object.keys(b); return keysA.length === keysB.length && keysA.every(function (key) { return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { return String(a) === String(b) } else { return false } } function looseIndexOf (arr, val) { for (var i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) { return i } } return -1 } /** * Ensure a function is called only once. */ function once (fn) { var called = false; return function () { if (!called) { called = true; fn.apply(this, arguments); } } } var SSR_ATTR = 'data-server-rendered'; var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]; /* */ var config = ({ /** * Option merge strategies (used in core/util/options) */ optionMergeStrategies: Object.create(null), /** * Whether to suppress warnings. */ silent: false, /** * Show production mode tip message on boot? */ productionTip: "development" !== 'production', /** * Whether to enable devtools */ devtools: "development" !== 'production', /** * Whether to record perf */ performance: false, /** * Error handler for watcher errors */ errorHandler: null, /** * Warn handler for watcher warns */ warnHandler: null, /** * Ignore certain custom elements */ ignoredElements: [], /** * Custom user key aliases for v-on */ keyCodes: Object.create(null), /** * Check if a tag is reserved so that it cannot be registered as a * component. This is platform-dependent and may be overwritten. */ isReservedTag: no, /** * Check if an attribute is reserved so that it cannot be used as a component * prop. This is platform-dependent and may be overwritten. */ isReservedAttr: no, /** * Check if a tag is an unknown element. * Platform-dependent. */ isUnknownElement: no, /** * Get the namespace of an element */ getTagNamespace: noop, /** * Parse the real tag name for the specific platform. */ parsePlatformTagName: identity, /** * Check if an attribute must be bound using property, e.g. value * Platform-dependent. */ mustUseProp: no, /** * Exposed for legacy reasons */ _lifecycleHooks: LIFECYCLE_HOOKS }); /* */ /** * Check if a string starts with $ or _ */ function isReserved (str) { var c = (str + '').charCodeAt(0); return c === 0x24 || c === 0x5F } /** * Define a property. */ function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); } /** * Parse simple path. */ var bailRE = /[^\w.$]/; function parsePath (path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } } /* */ // can we use __proto__? var hasProto = '__proto__' in {}; // Browser environment sniffing var inBrowser = typeof window !== 'undefined'; var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); var UA = inBrowser && window.navigator.userAgent.toLowerCase(); var isIE = UA && /msie|trident/.test(UA); var isIE9 = UA && UA.indexOf('msie 9.0') > 0; var isEdge = UA && UA.indexOf('edge/') > 0; var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; // Firefox has a "watch" function on Object.prototype... var nativeWatch = ({}).watch; var supportsPassive = false; if (inBrowser) { try { var opts = {}; Object.defineProperty(opts, 'passive', ({ get: function get () { /* istanbul ignore next */ supportsPassive = true; } })); // https://github.com/facebook/flow/issues/285 window.addEventListener('test-passive', null, opts); } catch (e) {} } // this needs to be lazy-evaled because vue may be required before // vue-server-renderer can set VUE_ENV var _isServer; var isServerRendering = function () { if (_isServer === undefined) { /* istanbul ignore if */ if (!inBrowser && typeof global !== 'undefined') { // detect presence of vue-server-renderer and avoid // Webpack shimming the process _isServer = global['process'].env.VUE_ENV === 'server'; } else { _isServer = false; } } return _isServer }; // detect devtools var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; /* istanbul ignore next */ function isNative (Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } var hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) && typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); var _Set; /* istanbul ignore if */ // $flow-disable-line if (typeof Set !== 'undefined' && isNative(Set)) { // use native Set when available. _Set = Set; } else { // a non-standard Set polyfill that only works with primitive keys. _Set = (function () { function Set () { this.set = Object.create(null); } Set.prototype.has = function has (key) { return this.set[key] === true }; Set.prototype.add = function add (key) { this.set[key] = true; }; Set.prototype.clear = function clear () { this.set = Object.create(null); }; return Set; }()); } /* */ var warn = noop; var tip = noop; var generateComponentTrace = (noop); // work around flow check var formatComponentName = (noop); { var hasConsole = typeof console !== 'undefined'; var classifyRE = /(?:^|[-_])(\w)/g; var classify = function (str) { return str .replace(classifyRE, function (c) { return c.toUpperCase(); }) .replace(/[-_]/g, ''); }; warn = function (msg, vm) { var trace = vm ? generateComponentTrace(vm) : ''; if (config.warnHandler) { config.warnHandler.call(null, msg, vm, trace); } else if (hasConsole && (!config.silent)) { console.error(("[Vue warn]: " + msg + trace)); } }; tip = function (msg, vm) { if (hasConsole && (!config.silent)) { console.warn("[Vue tip]: " + msg + ( vm ? generateComponentTrace(vm) : '' )); } }; formatComponentName = function (vm, includeFile) { if (vm.$root === vm) { return '' } var options = typeof vm === 'function' && vm.cid != null ? vm.options : vm._isVue ? vm.$options || vm.constructor.options : vm || {}; var name = options.name || options._componentTag; var file = options.__file; if (!name && file) { var match = file.match(/([^/\\]+)\.vue$/); name = match && match[1]; } return ( (name ? ("<" + (classify(name)) + ">") : "") + (file && includeFile !== false ? (" at " + file) : '') ) }; var repeat = function (str, n) { var res = ''; while (n) { if (n % 2 === 1) { res += str; } if (n > 1) { str += str; } n >>= 1; } return res }; generateComponentTrace = function (vm) { if (vm._isVue && vm.$parent) { var tree = []; var currentRecursiveSequence = 0; while (vm) { if (tree.length > 0) { var last = tree[tree.length - 1]; if (last.constructor === vm.constructor) { currentRecursiveSequence++; vm = vm.$parent; continue } else if (currentRecursiveSequence > 0) { tree[tree.length - 1] = [last, currentRecursiveSequence]; currentRecursiveSequence = 0; } } tree.push(vm); vm = vm.$parent; } return '\n\nfound in\n\n' + tree .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") : formatComponentName(vm))); }) .join('\n') } else { return ("\n\n(found in " + (formatComponentName(vm)) + ")") } }; } /* */ var uid = 0; /** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null; var targetStack = []; function pushTarget (_target) { if (Dep.target) { targetStack.push(Dep.target); } Dep.target = _target; } function popTarget () { Dep.target = targetStack.pop(); } /* */ var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag; this.data = data; this.children = children; this.text = text; this.elm = elm; this.ns = undefined; this.context = context; this.fnContext = undefined; this.fnOptions = undefined; this.fnScopeId = undefined; this.key = data && data.key; this.componentOptions = componentOptions; this.componentInstance = undefined; this.parent = undefined; this.raw = false; this.isStatic = false; this.isRootInsert = true; this.isComment = false; this.isCloned = false; this.isOnce = false; this.asyncFactory = asyncFactory; this.asyncMeta = undefined; this.isAsyncPlaceholder = false; }; var prototypeAccessors = { child: { configurable: true } }; // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ prototypeAccessors.child.get = function () { return this.componentInstance }; Object.defineProperties( VNode.prototype, prototypeAccessors ); var createEmptyVNode = function (text) { if ( text === void 0 ) text = ''; var node = new VNode(); node.text = text; node.isComment = true; return node }; function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val)) } // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. function cloneVNode (vnode, deep) { var componentOptions = vnode.componentOptions; var cloned = new VNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, componentOptions, vnode.asyncFactory ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.isCloned = true; if (deep) { if (vnode.children) { cloned.children = cloneVNodes(vnode.children, true); } if (componentOptions && componentOptions.children) { componentOptions.children = cloneVNodes(componentOptions.children, true); } } return cloned } function cloneVNodes (vnodes, deep) { var len = vnodes.length; var res = new Array(len); for (var i = 0; i < len; i++) { res[i] = cloneVNode(vnodes[i], deep); } return res } /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); }); /* */ var arrayKeys = Object.getOwnPropertyNames(arrayMethods); /** * By default, when a reactive property is set, the new value is * also converted to become reactive. However when passing down props, * we don't want to force conversion because the value may be a nested value * under a frozen data structure. Converting it would defeat the optimization. */ var observerState = { shouldConvert: true }; /** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */ var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } }; /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } }; // helpers /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src, keys) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } } /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } /** * Define a reactive property on an Object. */ function defineReactive ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } /** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ function set (target, key, val) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (key in target && !(key in Object.prototype)) { target[key] = val; return val } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { "development" !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val } if (!ob) { target[key] = val; return val } defineReactive(ob.value, key, val); ob.dep.notify(); return val } /** * Delete a property and trigger change if necessary. */ function del (target, key) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1); return } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { "development" !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ); return } if (!hasOwn(target, key)) { return } delete target[key]; if (!ob) { return } ob.dep.notify(); } /** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray (value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } } /* */ /** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ var strats = config.optionMergeStrategies; /** * Options with restrictions */ { strats.el = strats.propsData = function (parent, child, vm, key) { if (!vm) { warn( "option \"" + key + "\" can only be used during instance " + 'creation with the `new` keyword.' ); } return defaultStrat(parent, child) }; } /** * Helper that recursively merges two data objects together. */ function mergeData (to, from) { if (!from) { return to } var key, toVal, fromVal; var keys = Object.keys(from); for (var i = 0; i < keys.length; i++) { key = keys[i]; toVal = to[key]; fromVal = from[key]; if (!hasOwn(to, key)) { set(to, key, fromVal); } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { mergeData(toVal, fromVal); } } return to } /** * Data */ function mergeDataOrFn ( parentVal, childVal, vm ) { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn () { // instance merge var instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } } strats.data = function ( parentVal, childVal, vm ) { if (!vm) { if (childVal && typeof childVal !== 'function') { "development" !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ); return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) }; /** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal, childVal ) { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(function (hook) { strats[hook] = mergeHook; }); /** * Assets * * When a vm is present (instance creation), we need to do * a three-way merge between constructor options, instance * options and parent options. */ function mergeAssets ( parentVal, childVal, vm, key ) { var res = Object.create(parentVal || null); if (childVal) { "development" !== 'production' && assertObjectType(key, childVal, vm); return extend(res, childVal) } else { return res } } ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets; }); /** * Watchers. * * Watchers hashes should not overwrite one * another, so we merge them as arrays. */ strats.watch = function ( parentVal, childVal, vm, key ) { // work around Firefox's Object.prototype.watch... if (parentVal === nativeWatch) { parentVal = undefined; } if (childVal === nativeWatch) { childVal = undefined; } /* istanbul ignore if */ if (!childVal) { return Object.create(parentVal || null) } { assertObjectType(key, childVal, vm); } if (!parentVal) { return childVal } var ret = {}; extend(ret, parentVal); for (var key$1 in childVal) { var parent = ret[key$1]; var child = childVal[key$1]; if (parent && !Array.isArray(parent)) { parent = [parent]; } ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; } return ret }; /** * Other object hashes. */ strats.props = strats.methods = strats.inject = strats.computed = function ( parentVal, childVal, vm, key ) { if (childVal && "development" !== 'production') { assertObjectType(key, childVal, vm); } if (!parentVal) { return childVal } var ret = Object.create(null); extend(ret, parentVal); if (childVal) { extend(ret, childVal); } return ret }; strats.provide = mergeDataOrFn; /** * Default strategy. */ var defaultStrat = function (parentVal, childVal) { return childVal === undefined ? parentVal : childVal }; /** * Validate component names */ function checkComponents (options) { for (var key in options.components) { validateComponentName(key); } } function validateComponentName (name) { if (!/^[a-zA-Z][\w-]*$/.test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characters and the hyphen, ' + 'and must start with a letter.' ); } var lower = name.toLowerCase(); if (isBuiltInTag(lower) || config.isReservedTag(lower)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ); } } /** * Ensure all props option syntax are normalized into the * Object-based format. */ function normalizeProps (options, vm) { var props = options.props; if (!props) { return } var res = {}; var i, val, name; if (Array.isArray(props)) { i = props.length; while (i--) { val = props[i]; if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else { warn('props must be strings when using array syntax.'); } } } else if (isPlainObject(props)) { for (var key in props) { val = props[key]; name = camelize(key); res[name] = isPlainObject(val) ? val : { type: val }; } } else { warn( "Invalid value for option \"props\": expected an Array or an Object, " + "but got " + (toRawType(props)) + ".", vm ); } options.props = res; } /** * Normalize all injections into Object-based format */ function normalizeInject (options, vm) { var inject = options.inject; var normalized = options.inject = {}; if (Array.isArray(inject)) { for (var i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] }; } } else if (isPlainObject(inject)) { for (var key in inject) { var val = inject[key]; normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; } } else if ("development" !== 'production' && inject) { warn( "Invalid value for option \"inject\": expected an Array or an Object, " + "but got " + (toRawType(inject)) + ".", vm ); } } /** * Normalize raw function directives into object format. */ function normalizeDirectives (options) { var dirs = options.directives; if (dirs) { for (var key in dirs) { var def = dirs[key]; if (typeof def === 'function') { dirs[key] = { bind: def, update: def }; } } } } function assertObjectType (name, value, vm) { if (!isPlainObject(value)) { warn( "Invalid value for option \"" + name + "\": expected an Object, " + "but got " + (toRawType(value)) + ".", vm ); } } /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ function mergeOptions ( parent, child, vm ) { { checkComponents(child); } if (typeof child === 'function') { child = child.options; } normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); var extendsFrom = child.extends; if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm); } if (child.mixins) { for (var i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField (key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options } /** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */ function resolveAsset ( options, type, id, warnMissing ) { /* istanbul ignore if */ if (typeof id !== 'string') { return } var assets = options[type]; // check local registration variations first if (hasOwn(assets, id)) { return assets[id] } var camelizedId = camelize(id); if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } var PascalCaseId = capitalize(camelizedId); if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } // fallback to prototype chain var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; if ("development" !== 'production' && warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ); } return res } /* */ function validateProp ( key, propOptions, propsData, vm ) { var prop = propOptions[key]; var absent = !hasOwn(propsData, key); var value = propsData[key]; // handle boolean props if (isType(Boolean, prop.type)) { if (absent && !hasOwn(prop, 'default')) { value = false; } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) { value = true; } } // check default value if (value === undefined) { value = getPropDefaultValue(vm, prop, key); // since the default value is a fresh copy, // make sure to observe it. var prevShouldConvert = observerState.shouldConvert; observerState.shouldConvert = true; observe(value); observerState.shouldConvert = prevShouldConvert; } { assertProp(prop, key, value, vm, absent); } return value } /** * Get the default value of a prop. */ function getPropDefaultValue (vm, prop, key) { // no default, return undefined if (!hasOwn(prop, 'default')) { return undefined } var def = prop.default; // warn against non-factory defaults for Object & Array if ("development" !== 'production' && isObject(def)) { warn( 'Invalid default value for prop "' + key + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm ); } // the raw prop value was also undefined from previous render, // return previous default value to avoid unnecessary watcher trigger if (vm && vm.$options.propsData && vm.$options.propsData[key] === undefined && vm._props[key] !== undefined ) { return vm._props[key] } // call factory function for non-Function types // a value is Function if its prototype is function even across different execution context return typeof def === 'function' && getType(prop.type) !== 'Function' ? def.call(vm) : def } /** * Assert whether a prop is valid. */ function assertProp ( prop, name, value, vm, absent ) { if (prop.required && absent) { warn( 'Missing required prop: "' + name + '"', vm ); return } if (value == null && !prop.required) { return } var type = prop.type; var valid = !type || type === true; var expectedTypes = []; if (type) { if (!Array.isArray(type)) { type = [type]; } for (var i = 0; i < type.length && !valid; i++) { var assertedType = assertType(value, type[i]); expectedTypes.push(assertedType.expectedType || ''); valid = assertedType.valid; } } if (!valid) { warn( "Invalid prop: type check failed for prop \"" + name + "\"." + " Expected " + (expectedTypes.map(capitalize).join(', ')) + ", got " + (toRawType(value)) + ".", vm ); return } var validator = prop.validator; if (validator) { if (!validator(value)) { warn( 'Invalid prop: custom validator check failed for prop "' + name + '".', vm ); } } } var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; function assertType (value, type) { var valid; var expectedType = getType(type); if (simpleCheckRE.test(expectedType)) { var t = typeof value; valid = t === expectedType.toLowerCase(); // for primitive wrapper objects if (!valid && t === 'object') { valid = value instanceof type; } } else if (expectedType === 'Object') { valid = isPlainObject(value); } else if (expectedType === 'Array') { valid = Array.isArray(value); } else { valid = value instanceof type; } return { valid: valid, expectedType: expectedType } } /** * Use function string name to check built-in types, * because a simple equality check will fail when running * across different vms / iframes. */ function getType (fn) { var match = fn && fn.toString().match(/^\s*function (\w+)/); return match ? match[1] : '' } function isType (type, fn) { if (!Array.isArray(fn)) { return getType(fn) === getType(type) } for (var i = 0, len = fn.length; i < len; i++) { if (getType(fn[i]) === getType(type)) { return true } } /* istanbul ignore next */ return false } /* */ function handleError (err, vm, info) { if (vm) { var cur = vm; while ((cur = cur.$parent)) { var hooks = cur.$options.errorCaptured; if (hooks) { for (var i = 0; i < hooks.length; i++) { try { var capture = hooks[i].call(cur, err, vm, info) === false; if (capture) { return } } catch (e) { globalHandleError(e, cur, 'errorCaptured hook'); } } } } } globalHandleError(err, vm, info); } function globalHandleError (err, vm, info) { if (config.errorHandler) { try { return config.errorHandler.call(null, err, vm, info) } catch (e) { logError(e, null, 'config.errorHandler'); } } logError(err, vm, info); } function logError (err, vm, info) { { warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); } /* istanbul ignore else */ if ((inBrowser || inWeex) && typeof console !== 'undefined') { console.error(err); } else { throw err } } /* */ /* globals MessageChannel */ var callbacks = []; var pending = false; function flushCallbacks () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } // Here we have async deferring wrappers using both micro and macro tasks. // In < 2.4 we used micro tasks everywhere, but there are some scenarios where // micro tasks have too high a priority and fires in between supposedly // sequential events (e.g. #4521, #6690) or even between bubbling of the same // event (#6566). However, using macro tasks everywhere also has subtle problems // when state is changed right before repaint (e.g. #6813, out-in transitions). // Here we use micro task by default, but expose a way to force macro task when // needed (e.g. in event handlers attached by v-on). var microTimerFunc; var macroTimerFunc; var useMacroTask = false; // Determine (macro) Task defer implementation. // Technically setImmediate should be the ideal choice, but it's only available // in IE. The only polyfill that consistently queues the callback after all DOM // events triggered in the same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = function () { setImmediate(flushCallbacks); }; } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { var channel = new MessageChannel(); var port = channel.port2; channel.port1.onmessage = flushCallbacks; macroTimerFunc = function () { port.postMessage(1); }; } else { /* istanbul ignore next */ macroTimerFunc = function () { setTimeout(flushCallbacks, 0); }; } // Determine MicroTask defer implementation. /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); microTimerFunc = function () { p.then(flushCallbacks); // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; } else { // fallback to macro microTimerFunc = macroTimerFunc; } /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a Task instead of a MicroTask. */ function withMacroTask (fn) { return fn._withTask || (fn._withTask = function () { useMacroTask = true; var res = fn.apply(null, arguments); useMacroTask = false; return res }) } function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; if (useMacroTask) { macroTimerFunc(); } else { microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } } /* */ var mark; var measure; { var perf = inBrowser && window.performance; /* istanbul ignore if */ if ( perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures ) { mark = function (tag) { return perf.mark(tag); }; measure = function (name, startTag, endTag) { perf.measure(name, startTag, endTag); perf.clearMarks(startTag); perf.clearMarks(endTag); perf.clearMeasures(name); }; } } /* not type checking this file because flow doesn't play well with Proxy */ var initProxy; { var allowedGlobals = makeMap( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 'require' // for Webpack/Browserify ); var warnNonPresent = function (target, key) { warn( "Property or method \"" + key + "\" is not defined on the instance but " + 'referenced during render. Make sure that this property is reactive, ' + 'either in the data option, or for class-based components, by ' + 'initializing the property. ' + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target ); }; var hasProxy = typeof Proxy !== 'undefined' && Proxy.toString().match(/native code/); if (hasProxy) { var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); config.keyCodes = new Proxy(config.keyCodes, { set: function set (target, key, value) { if (isBuiltInModifier(key)) { warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); return false } else { target[key] = value; return true } } }); } var hasHandler = { has: function has (target, key) { var has = key in target; var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; if (!has && !isAllowed) { warnNonPresent(target, key); } return has || !isAllowed } }; var getHandler = { get: function get (target, key) { if (typeof key === 'string' && !(key in target)) { warnNonPresent(target, key); } return target[key] } }; initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; } }; } /* */ var seenObjects = new _Set(); /** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */ function traverse (val) { _traverse(val, seenObjects); seenObjects.clear(); } function _traverse (val, seen) { var i, keys; var isA = Array.isArray(val); if ((!isA && !isObject(val)) || Object.isFrozen(val)) { return } if (val.__ob__) { var depId = val.__ob__.dep.id; if (seen.has(depId)) { return } seen.add(depId); } if (isA) { i = val.length; while (i--) { _traverse(val[i], seen); } } else { keys = Object.keys(val); i = keys.length; while (i--) { _traverse(val[keys[i]], seen); } } } /* */ var normalizeEvent = cached(function (name) { var passive = name.charAt(0) === '&'; name = passive ? name.slice(1) : name; var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first name = once$$1 ? name.slice(1) : name; var capture = name.charAt(0) === '!'; name = capture ? name.slice(1) : name; return { name: name, once: once$$1, capture: capture, passive: passive } }); function createFnInvoker (fns) { function invoker () { var arguments$1 = arguments; var fns = invoker.fns; if (Array.isArray(fns)) { var cloned = fns.slice(); for (var i = 0; i < cloned.length; i++) { cloned[i].apply(null, arguments$1); } } else { // return handler return value for single handlers return fns.apply(null, arguments) } } invoker.fns = fns; return invoker } function updateListeners ( on, oldOn, add, remove$$1, vm ) { var name, cur, old, event; for (name in on) { cur = on[name]; old = oldOn[name]; event = normalizeEvent(name); if (isUndef(cur)) { "development" !== 'production' && warn( "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), vm ); } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur); } add(event.name, cur, event.once, event.capture, event.passive); } else if (cur !== old) { old.fns = cur; on[name] = old; } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove$$1(event.name, oldOn[name], event.capture); } } } /* */ function mergeVNodeHook (def, hookKey, hook) { if (def instanceof VNode) { def = def.data.hook || (def.data.hook = {}); } var invoker; var oldHook = def[hookKey]; function wrappedHook () { hook.apply(this, arguments); // important: remove merged hook to ensure it's called only once // and prevent memory leak remove(invoker.fns, wrappedHook); } if (isUndef(oldHook)) { // no existing hook invoker = createFnInvoker([wrappedHook]); } else { /* istanbul ignore if */ if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { // already a merged invoker invoker = oldHook; invoker.fns.push(wrappedHook); } else { // existing plain hook invoker = createFnInvoker([oldHook, wrappedHook]); } } invoker.merged = true; def[hookKey] = invoker; } /* */ function extractPropsFromVNodeData ( data, Ctor, tag ) { // we are only extracting raw values here. // validation and default values are handled in the child // component itself. var propOptions = Ctor.options.props; if (isUndef(propOptions)) { return } var res = {}; var attrs = data.attrs; var props = data.props; if (isDef(attrs) || isDef(props)) { for (var key in propOptions) { var altKey = hyphenate(key); { var keyInLowerCase = key.toLowerCase(); if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { tip( "Prop \"" + keyInLowerCase + "\" is passed to component " + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + " \"" + key + "\". " + "Note that HTML attributes are case-insensitive and camelCased " + "props need to use their kebab-case equivalents when using in-DOM " + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." ); } } checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false); } } return res } function checkProp ( res, hash, key, altKey, preserve ) { if (isDef(hash)) { if (hasOwn(hash, key)) { res[key] = hash[key]; if (!preserve) { delete hash[key]; } return true } else if (hasOwn(hash, altKey)) { res[key] = hash[altKey]; if (!preserve) { delete hash[altKey]; } return true } } return false } /* */ // The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array. There are // two cases where extra normalization is needed: // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. function simpleNormalizeChildren (children) { for (var i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g.