From f964e0e502d100abff04b64a766e2be3794c068a Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Sat, 22 Jan 2022 19:34:30 -0500 Subject: [PATCH] added router support for url fragments, added language attribute to HTML document tag to improve validation, fixed Theme Settings so they can only be invoked via the Control Panel, added support for webp image files --- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 33 +- .../Controls/Container/ModuleTitle.razor | 4 +- Oqtane.Client/UI/Interop.cs | 14 + Oqtane.Client/UI/SiteRouter.razor | 489 +++++++++--------- Oqtane.Server/Pages/_Host.cshtml | 2 +- Oqtane.Server/Pages/_Host.cshtml.cs | 23 +- Oqtane.Server/Repository/ThemeRepository.cs | 21 +- Oqtane.Server/wwwroot/css/app.css | 4 + Oqtane.Server/wwwroot/js/interop.js | 11 +- Oqtane.Shared/Shared/Constants.cs | 2 +- 10 files changed, 336 insertions(+), 267 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 5f2a8536..d2edd289 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -384,21 +384,24 @@ private void ThemeSettings() { _themeSettingsType = null; - var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); - if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) - { - _themeSettingsType = Type.GetType(theme.ThemeSettingsType); - if (_themeSettingsType != null) - { - ThemeSettingsComponent = builder => - { - builder.OpenComponent(0, _themeSettingsType); - builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - builder.CloseComponent(); - }; - } - _refresh = true; - } + if (PageState.QueryString.ContainsKey("cp")) // can only be displayed if invoked from Control Panel + { + var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); + if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) + { + _themeSettingsType = Type.GetType(theme.ThemeSettingsType); + if (_themeSettingsType != null) + { + ThemeSettingsComponent = builder => + { + builder.OpenComponent(0, _themeSettingsType); + builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); + builder.CloseComponent(); + }; + } + _refresh = true; + } + } } private async Task SavePage() diff --git a/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor b/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor index 24a5da86..a2d9ba7c 100644 --- a/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor +++ b/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor @@ -3,7 +3,9 @@ @attribute [OqtaneIgnore] - @((MarkupString)title) + + @((MarkupString)title) + @code { diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 28b72307..43325047 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -279,5 +279,19 @@ namespace Oqtane.UI } } + public Task ScrollToId(string id) + { + try + { + _jsRuntime.InvokeVoidAsync( + "Oqtane.Interop.scrollToId", + id); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 360ff6a1..ad520fb5 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -11,6 +11,7 @@ @inject IModuleService ModuleService @inject IUrlMappingService UrlMappingService @inject ILogService LogService +@inject IJSRuntime JSRuntime @implements IHandleAfterRender @DynamicComponent @@ -234,6 +235,7 @@ }; OnStateChange?.Invoke(_pagestate); + await ScrollToFragment(_pagestate.Uri); } } else // page not found @@ -242,7 +244,7 @@ var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath); if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) { - var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; + var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; NavigationManager.NavigateTo(url, false); } else // not mapped @@ -262,238 +264,259 @@ } } } - } + } + } + else + { + // site does not exist + } + } + + private async void LocationChanged(object sender, LocationChangedEventArgs args) + { + _absoluteUri = args.Location; + await Refresh(); + } + + Task IHandleAfterRender.OnAfterRenderAsync() + { + if (!_navigationInterceptionEnabled) + { + _navigationInterceptionEnabled = true; + return NavigationInterception.EnableNavigationInterceptionAsync(); + } + return Task.CompletedTask; + } + + private Dictionary ParseQueryString(string query) + { + Dictionary querystring = new Dictionary(StringComparer.OrdinalIgnoreCase); // case insensistive keys + if (!string.IsNullOrEmpty(query)) + { + if (query.StartsWith("?")) + { + query = query.Substring(1); // ignore "?" + } + foreach (string kvp in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (kvp != "") + { + if (kvp.Contains("=")) + { + string[] pair = kvp.Split('='); + querystring.Add(pair[0], pair[1]); + } + else + { + querystring.Add(kvp, "true"); // default parameter when no value is provided + } + } + } + } + return querystring; + } + + private async Task ProcessPage(Page page, Site site, User user) + { + try + { + if (page.IsPersonalizable && user != null) + { + // load the personalized page + page = await PageService.GetPageAsync(page.PageId, user.UserId); + } + + if (string.IsNullOrEmpty(page.ThemeType)) + { + page.ThemeType = site.DefaultThemeType; + } + + page.Panes = new List(); + page.Resources = new List(); + + string panes = PaneNames.Admin; + Type themetype = Type.GetType(page.ThemeType); + if (themetype == null) + { + // fallback + page.ThemeType = Constants.DefaultTheme; + themetype = Type.GetType(Constants.DefaultTheme); + } + if (themetype != null) + { + var themeobject = Activator.CreateInstance(themetype) as IThemeControl; + if (themeobject != null) + { + if (!string.IsNullOrEmpty(themeobject.Panes)) + { + panes = themeobject.Panes; + } + page.Resources = ManagePageResources(page.Resources, themeobject.Resources); + } + } + page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + catch + { + // error loading theme or layout + } + + return page; + } + + private (Page Page, List Modules) ProcessModules(Page page, List modules, int moduleid, string action, string defaultcontainertype) + { + var paneindex = new Dictionary(); + foreach (Module module in modules) + { + // initialize module control properties + module.SecurityAccessLevel = SecurityAccessLevel.Host; + module.ControlTitle = ""; + module.Actions = ""; + module.UseAdminContainer = false; + module.PaneModuleIndex = -1; + module.PaneModuleCount = 0; + + if ((module.PageId == page.PageId || module.ModuleId == moduleid)) + { + var typename = Constants.ErrorModule; + if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) + { + typename = module.ModuleDefinition.ControlTypeTemplate; + + // handle default action + if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) + { + action = module.ModuleDefinition.DefaultAction; + } + + // check if the module defines custom action routes + if (module.ModuleDefinition.ControlTypeRoutes != "") + { + foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (route.StartsWith(action + "=")) + { + typename = route.Replace(action + "=", ""); + } + } + } + } + + // ensure component exists and implements IModuleControl + module.ModuleType = ""; + if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase)) + { + typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action); + } + else + { + typename = typename.Replace(Constants.ActionToken, action); + } + Type moduletype = Type.GetType(typename, false, true); // case insensitive + if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) + { + module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name + } + + // get additional metadata from IModuleControl interface + if (moduletype != null && module.ModuleType != "") + { + // retrieve module component resources + var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); + if (action.ToLower() == "settings" && module.ModuleDefinition != null) + { + // settings components are embedded within a framework settings module + moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); + if (moduletype != null) + { + moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); + } + } + + // additional metadata needed for admin components + if (module.ModuleId == moduleid && action != "") + { + module.SecurityAccessLevel = moduleobject.SecurityAccessLevel; + module.ControlTitle = moduleobject.Title; + module.Actions = moduleobject.Actions; + module.UseAdminContainer = moduleobject.UseAdminContainer; + } + } + + // ensure module's pane exists in current page and if not, assign it to the Admin pane + if (page.Panes == null || page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1) + { + module.Pane = PaneNames.Admin; + } + + // calculate module position within pane + if (paneindex.ContainsKey(module.Pane.ToLower())) + { + paneindex[module.Pane.ToLower()] += 1; + } + else + { + paneindex.Add(module.Pane.ToLower(), 0); + } + + module.PaneModuleIndex = paneindex[module.Pane.ToLower()]; + + // container fallback + if (string.IsNullOrEmpty(module.ContainerType)) + { + module.ContainerType = defaultcontainertype; + } + } + } + + foreach (Module module in modules.Where(item => item.PageId == page.PageId)) + { + if (paneindex.ContainsKey(module.Pane.ToLower())) + { + module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1; + } + } + + return (page, modules); + } + + private List ManagePageResources(List pageresources, List resources) + { + if (resources != null) + { + foreach (var resource in resources) + { + // ensure resource does not exist already + if (pageresources.Find(item => item.Url == resource.Url) == null) + { + pageresources.Add(resource); + } + } + } + return pageresources; + } + + private async Task ScrollToFragment(Uri uri) + { + var fragment = uri.Fragment; + if (fragment.StartsWith('#')) + { + // handle text fragment (https://example.org/#test:~:text=foo) + var id = fragment.Substring(1); + var index = id.IndexOf(":~:", StringComparison.Ordinal); + if (index > 0) + { + id = id.Substring(0, index); + } + + if (!string.IsNullOrEmpty(id)) + { + var interop = new Interop(JSRuntime); + await interop.ScrollToId(id); + } } - else - { - // site does not exist - } - } - - private async void LocationChanged(object sender, LocationChangedEventArgs args) - { - _absoluteUri = args.Location; - await Refresh(); - } - - Task IHandleAfterRender.OnAfterRenderAsync() - { - if (!_navigationInterceptionEnabled) - { - _navigationInterceptionEnabled = true; - return NavigationInterception.EnableNavigationInterceptionAsync(); - } - return Task.CompletedTask; - } - - private Dictionary ParseQueryString(string query) - { - Dictionary querystring = new Dictionary(StringComparer.OrdinalIgnoreCase); // case insensistive keys - if (!string.IsNullOrEmpty(query)) - { - if (query.StartsWith("?")) - { - query = query.Substring(1); // ignore "?" - } - foreach (string kvp in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (kvp != "") - { - if (kvp.Contains("=")) - { - string[] pair = kvp.Split('='); - querystring.Add(pair[0], pair[1]); - } - else - { - querystring.Add(kvp, "true"); // default parameter when no value is provided - } - } - } - } - return querystring; - } - - private async Task ProcessPage(Page page, Site site, User user) - { - try - { - if (page.IsPersonalizable && user != null) - { - // load the personalized page - page = await PageService.GetPageAsync(page.PageId, user.UserId); - } - - if (string.IsNullOrEmpty(page.ThemeType)) - { - page.ThemeType = site.DefaultThemeType; - } - - page.Panes = new List(); - page.Resources = new List(); - - string panes = PaneNames.Admin; - Type themetype = Type.GetType(page.ThemeType); - if (themetype == null) - { - // fallback - page.ThemeType = Constants.DefaultTheme; - themetype = Type.GetType(Constants.DefaultTheme); - } - if (themetype != null) - { - var themeobject = Activator.CreateInstance(themetype) as IThemeControl; - if (themeobject != null) - { - if (!string.IsNullOrEmpty(themeobject.Panes)) - { - panes = themeobject.Panes; - } - page.Resources = ManagePageResources(page.Resources, themeobject.Resources); - } - } - page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - catch - { - // error loading theme or layout - } - - return page; - } - - private (Page Page, List Modules) ProcessModules(Page page, List modules, int moduleid, string action, string defaultcontainertype) - { - var paneindex = new Dictionary(); - foreach (Module module in modules) - { - // initialize module control properties - module.SecurityAccessLevel = SecurityAccessLevel.Host; - module.ControlTitle = ""; - module.Actions = ""; - module.UseAdminContainer = false; - module.PaneModuleIndex = -1; - module.PaneModuleCount = 0; - - if ((module.PageId == page.PageId || module.ModuleId == moduleid)) - { - var typename = Constants.ErrorModule; - if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) - { - typename = module.ModuleDefinition.ControlTypeTemplate; - - // handle default action - if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) - { - action = module.ModuleDefinition.DefaultAction; - } - - // check if the module defines custom action routes - if (module.ModuleDefinition.ControlTypeRoutes != "") - { - foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (route.StartsWith(action + "=")) - { - typename = route.Replace(action + "=", ""); - } - } - } - } - - // ensure component exists and implements IModuleControl - module.ModuleType = ""; - if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase)) - { - typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action); - } - else - { - typename = typename.Replace(Constants.ActionToken, action); - } - Type moduletype = Type.GetType(typename, false, true); // case insensitive - if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) - { - module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name - } - - // get additional metadata from IModuleControl interface - if (moduletype != null && module.ModuleType != "") - { - // retrieve module component resources - var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); - if (action.ToLower() == "settings" && module.ModuleDefinition != null) - { - // settings components are embedded within a framework settings module - moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); - if (moduletype != null) - { - moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); - } - } - - // additional metadata needed for admin components - if (module.ModuleId == moduleid && action != "") - { - module.SecurityAccessLevel = moduleobject.SecurityAccessLevel; - module.ControlTitle = moduleobject.Title; - module.Actions = moduleobject.Actions; - module.UseAdminContainer = moduleobject.UseAdminContainer; - } - } - - // ensure module's pane exists in current page and if not, assign it to the Admin pane - if (page.Panes == null || page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1) - { - module.Pane = PaneNames.Admin; - } - - // calculate module position within pane - if (paneindex.ContainsKey(module.Pane.ToLower())) - { - paneindex[module.Pane.ToLower()] += 1; - } - else - { - paneindex.Add(module.Pane.ToLower(), 0); - } - - module.PaneModuleIndex = paneindex[module.Pane.ToLower()]; - - // container fallback - if (string.IsNullOrEmpty(module.ContainerType)) - { - module.ContainerType = defaultcontainertype; - } - } - } - - foreach (Module module in modules.Where(item => item.PageId == page.PageId)) - { - if (paneindex.ContainsKey(module.Pane.ToLower())) - { - module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1; - } - } - - return (page, modules); - } - - private List ManagePageResources(List pageresources, List resources) - { - if (resources != null) - { - foreach (var resource in resources) - { - // ensure resource does not exist already - if (pageresources.Find(item => item.Url == resource.Url) == null) - { - pageresources.Add(resource); - } - } - } - return pageresources; } } diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 9d562067..0185a5c2 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -3,7 +3,7 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @model Oqtane.Pages.HostModel - + diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 1d9004de..1a882402 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -52,6 +52,7 @@ namespace Oqtane.Pages _settings = settings; } + public string Language = "en"; public string AntiForgeryToken = ""; public string Runtime = "Server"; public RenderMode RenderMode = RenderMode.Server; @@ -174,19 +175,29 @@ namespace Oqtane.Pages } // set culture if not specified - if (HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] == null) + string culture = HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; + if (culture == null) { - // set default language for site if the culture is not supported + // get default language for site var languages = _languages.GetLanguages(alias.SiteId); - if (languages.Any() && languages.All(l => l.Code != CultureInfo.CurrentUICulture.Name)) + if (languages.Any()) { - var defaultLanguage = languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First(); - SetLocalizationCookie(defaultLanguage.Code); + // use default language if specified otherwise use first language in collection + culture = (languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First()).Code; } else { - SetLocalizationCookie(_localizationManager.GetDefaultCulture()); + culture = _localizationManager.GetDefaultCulture(); } + SetLocalizationCookie(culture); + } + + // set language for page + if (!string.IsNullOrEmpty(culture)) + { + // localization cookie value in form of c=en|uic=en + Language = culture.Split('|')[0]; + Language = Language.Replace("c=", ""); } } } diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 1b3b4e77..7f72eb9c 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -125,15 +125,18 @@ namespace Oqtane.Repository foreach (Type containertype in containertypes) { var containerobject = Activator.CreateInstance(containertype) as IThemeControl; - theme.Containers.Add( - new ThemeControl - { - TypeName = containertype.FullName + ", " + themeControlType.Assembly.GetName().Name, - Name = (string.IsNullOrEmpty(containerobject.Name)) ? Utilities.GetTypeNameLastSegment(containertype.FullName, 0) : containerobject.Name, - Thumbnail = containerobject.Thumbnail, - Panes = "" - } - ); + if (theme.Containers.FirstOrDefault(item => item.TypeName == containertype.FullName + ", " + themeControlType.Assembly.GetName().Name) == null) + { + theme.Containers.Add( + new ThemeControl + { + TypeName = containertype.FullName + ", " + themeControlType.Assembly.GetName().Name, + Name = (string.IsNullOrEmpty(containerobject.Name)) ? Utilities.GetTypeNameLastSegment(containertype.FullName, 0) : containerobject.Name, + Thumbnail = containerobject.Thumbnail, + Panes = "" + } + ); + } } themes[index] = theme; diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index a3559989..5da25ceb 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -126,6 +126,10 @@ app { margin-bottom: 15px; } +.app-moduletitle a { + scroll-margin-top: 7rem; +} + /* Tooltips */ .app-tooltip { cursor: help; diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index e4e07e8e..dcecde3c 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -376,5 +376,14 @@ Oqtane.Interop = { left: left, behavior: behavior }); + }, + scrollToId: function (id) { + var element = document.getElementById(id); + if (element instanceof HTMLElement) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); } -}; +}}; diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 0085b45f..aa70e703 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -62,7 +62,7 @@ namespace Oqtane.Shared { [Obsolete(RoleObsoleteMessage)] public const string RegisteredRole = RoleNames.Registered; - public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico"; + public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp"; public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$";