From 95e21317e263316895444e16955cefdb43be78a2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 24 Oct 2023 12:25:52 +0200 Subject: [PATCH] Updates for new document format The documentation has been moved to doc/ and updated to be published to read the doc with in particular table of content. The documentation is now parsed to follow the ToC, point the images to the right addresses and remove internal links. --- adsys.pb.go | 123 +++++++------- adsys.proto | 8 +- adsys_grpc.pb.go | 63 +++---- cmd/adsysd/client/doc.go | 100 +++-------- internal/adsysservice/doc.go | 314 ++++++++++++++++++++++++----------- 5 files changed, 330 insertions(+), 278 deletions(-) diff --git a/adsys.pb.go b/adsys.pb.go index 9a5ddae6e..8e603f17a 100644 --- a/adsys.pb.go +++ b/adsys.pb.go @@ -1,12 +1,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.29.1 +// protoc-gen-go v1.25.0 // protoc v3.21.12 // source: adsys.proto package adsys import ( + proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -20,6 +21,10 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + type Empty struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -506,16 +511,16 @@ func (x *GetDocRequest) GetChapter() string { return "" } -type ListDocRequest struct { +type ListDocReponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Raw bool `protobuf:"varint,1,opt,name=raw,proto3" json:"raw,omitempty"` // Set to true to not format the output of the table of content + Chapters []string `protobuf:"bytes,1,rep,name=chapters,proto3" json:"chapters,omitempty"` } -func (x *ListDocRequest) Reset() { - *x = ListDocRequest{} +func (x *ListDocReponse) Reset() { + *x = ListDocReponse{} if protoimpl.UnsafeEnabled { mi := &file_adsys_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -523,13 +528,13 @@ func (x *ListDocRequest) Reset() { } } -func (x *ListDocRequest) String() string { +func (x *ListDocReponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListDocRequest) ProtoMessage() {} +func (*ListDocReponse) ProtoMessage() {} -func (x *ListDocRequest) ProtoReflect() protoreflect.Message { +func (x *ListDocReponse) ProtoReflect() protoreflect.Message { mi := &file_adsys_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -541,16 +546,16 @@ func (x *ListDocRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListDocRequest.ProtoReflect.Descriptor instead. -func (*ListDocRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ListDocReponse.ProtoReflect.Descriptor instead. +func (*ListDocReponse) Descriptor() ([]byte, []int) { return file_adsys_proto_rawDescGZIP(), []int{9} } -func (x *ListDocRequest) GetRaw() bool { +func (x *ListDocReponse) GetChapters() []string { if x != nil { - return x.Raw + return x.Chapters } - return false + return nil } var File_adsys_proto protoreflect.FileDescriptor @@ -593,49 +598,49 @@ var file_adsys_proto_rawDesc = []byte{ 0x12, 0x0a, 0x04, 0x61, 0x64, 0x6d, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x6d, 0x6c, 0x22, 0x29, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x22, 0x22, - 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, - 0x61, 0x77, 0x32, 0xc9, 0x04, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, - 0x0a, 0x03, 0x43, 0x61, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x22, 0x2c, + 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x73, 0x32, 0xc0, 0x04, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x43, 0x61, 0x74, 0x12, + 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x24, 0x0a, 0x07, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, + 0x12, 0x23, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x1e, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x0c, 0x2e, + 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x06, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x30, 0x01, 0x12, 0x2e, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x14, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x06, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, + 0x0a, 0x17, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x44, 0x65, + 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x44, 0x75, 0x6d, 0x70, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x2b, 0x0a, 0x06, 0x47, 0x65, + 0x74, 0x44, 0x6f, 0x63, 0x12, 0x0e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x24, 0x0a, 0x07, 0x4c, 0x69, 0x73, 0x74, 0x44, + 0x6f, 0x63, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, + 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x11, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, - 0x12, 0x24, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x23, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x1e, 0x0a, 0x04, 0x53, - 0x74, 0x6f, 0x70, 0x12, 0x0c, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x30, 0x01, 0x12, 0x2e, 0x0a, 0x0c, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x14, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x44, - 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x75, - 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x17, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x69, 0x65, 0x73, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x1d, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x65, 0x66, 0x69, - 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, - 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x65, 0x66, 0x69, 0x6e, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, - 0x12, 0x2b, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x12, 0x0e, 0x2e, 0x47, 0x65, 0x74, - 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x2d, 0x0a, - 0x07, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x63, 0x12, 0x0f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, - 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x09, - 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x11, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x53, - 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, - 0x2a, 0x0a, 0x0d, 0x47, 0x50, 0x4f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x14, 0x43, - 0x65, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x53, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x19, - 0x5a, 0x17, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, - 0x6e, 0x74, 0x75, 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x12, 0x2a, 0x0a, 0x0d, 0x47, 0x50, 0x4f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x14, + 0x43, 0x65, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x53, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, + 0x19, 0x5a, 0x17, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, + 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -661,7 +666,7 @@ var file_adsys_proto_goTypes = []interface{}{ (*DumpPolicyDefinitionsRequest)(nil), // 6: DumpPolicyDefinitionsRequest (*DumpPolicyDefinitionsResponse)(nil), // 7: DumpPolicyDefinitionsResponse (*GetDocRequest)(nil), // 8: GetDocRequest - (*ListDocRequest)(nil), // 9: ListDocRequest + (*ListDocReponse)(nil), // 9: ListDocReponse } var file_adsys_proto_depIdxs = []int32{ 0, // 0: service.Cat:input_type -> Empty @@ -672,7 +677,7 @@ var file_adsys_proto_depIdxs = []int32{ 5, // 5: service.DumpPolicies:input_type -> DumpPoliciesRequest 6, // 6: service.DumpPoliciesDefinitions:input_type -> DumpPolicyDefinitionsRequest 8, // 7: service.GetDoc:input_type -> GetDocRequest - 9, // 8: service.ListDoc:input_type -> ListDocRequest + 0, // 8: service.ListDoc:input_type -> Empty 1, // 9: service.ListUsers:input_type -> ListUsersRequest 0, // 10: service.GPOListScript:input_type -> Empty 0, // 11: service.CertAutoEnrollScript:input_type -> Empty @@ -684,7 +689,7 @@ var file_adsys_proto_depIdxs = []int32{ 3, // 17: service.DumpPolicies:output_type -> StringResponse 7, // 18: service.DumpPoliciesDefinitions:output_type -> DumpPolicyDefinitionsResponse 3, // 19: service.GetDoc:output_type -> StringResponse - 3, // 20: service.ListDoc:output_type -> StringResponse + 9, // 20: service.ListDoc:output_type -> ListDocReponse 3, // 21: service.ListUsers:output_type -> StringResponse 3, // 22: service.GPOListScript:output_type -> StringResponse 3, // 23: service.CertAutoEnrollScript:output_type -> StringResponse @@ -810,7 +815,7 @@ func file_adsys_proto_init() { } } file_adsys_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListDocRequest); i { + switch v := v.(*ListDocReponse); i { case 0: return &v.state case 1: diff --git a/adsys.proto b/adsys.proto index 307b28ca5..28e062f43 100644 --- a/adsys.proto +++ b/adsys.proto @@ -11,7 +11,7 @@ service service { rpc DumpPolicies(DumpPoliciesRequest) returns (stream StringResponse); rpc DumpPoliciesDefinitions(DumpPolicyDefinitionsRequest) returns (stream DumpPolicyDefinitionsResponse); rpc GetDoc(GetDocRequest) returns (stream StringResponse); - rpc ListDoc(ListDocRequest) returns (stream StringResponse); + rpc ListDoc(Empty) returns (stream ListDocReponse); rpc ListUsers(ListUsersRequest) returns (stream StringResponse); rpc GPOListScript(Empty) returns (stream StringResponse); rpc CertAutoEnrollScript(Empty) returns (stream StringResponse); @@ -60,6 +60,6 @@ message GetDocRequest { string chapter = 1; } -message ListDocRequest { - bool raw = 1; // Set to true to not format the output of the table of content -} +message ListDocReponse { + repeated string chapters = 1; +} \ No newline at end of file diff --git a/adsys_grpc.pb.go b/adsys_grpc.pb.go index 1bc7b2b16..dbbc37f61 100644 --- a/adsys_grpc.pb.go +++ b/adsys_grpc.pb.go @@ -1,8 +1,4 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.3.0 -// - protoc v3.21.12 -// source: adsys.proto package adsys @@ -18,21 +14,6 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 -const ( - Service_Cat_FullMethodName = "/service/Cat" - Service_Version_FullMethodName = "/service/Version" - Service_Status_FullMethodName = "/service/Status" - Service_Stop_FullMethodName = "/service/Stop" - Service_UpdatePolicy_FullMethodName = "/service/UpdatePolicy" - Service_DumpPolicies_FullMethodName = "/service/DumpPolicies" - Service_DumpPoliciesDefinitions_FullMethodName = "/service/DumpPoliciesDefinitions" - Service_GetDoc_FullMethodName = "/service/GetDoc" - Service_ListDoc_FullMethodName = "/service/ListDoc" - Service_ListUsers_FullMethodName = "/service/ListUsers" - Service_GPOListScript_FullMethodName = "/service/GPOListScript" - Service_CertAutoEnrollScript_FullMethodName = "/service/CertAutoEnrollScript" -) - // ServiceClient is the client API for Service service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -45,7 +26,7 @@ type ServiceClient interface { DumpPolicies(ctx context.Context, in *DumpPoliciesRequest, opts ...grpc.CallOption) (Service_DumpPoliciesClient, error) DumpPoliciesDefinitions(ctx context.Context, in *DumpPolicyDefinitionsRequest, opts ...grpc.CallOption) (Service_DumpPoliciesDefinitionsClient, error) GetDoc(ctx context.Context, in *GetDocRequest, opts ...grpc.CallOption) (Service_GetDocClient, error) - ListDoc(ctx context.Context, in *ListDocRequest, opts ...grpc.CallOption) (Service_ListDocClient, error) + ListDoc(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_ListDocClient, error) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (Service_ListUsersClient, error) GPOListScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_GPOListScriptClient, error) CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) @@ -60,7 +41,7 @@ func NewServiceClient(cc grpc.ClientConnInterface) ServiceClient { } func (c *serviceClient) Cat(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CatClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[0], Service_Cat_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[0], "/service/Cat", opts...) if err != nil { return nil, err } @@ -92,7 +73,7 @@ func (x *serviceCatClient) Recv() (*StringResponse, error) { } func (c *serviceClient) Version(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_VersionClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[1], Service_Version_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[1], "/service/Version", opts...) if err != nil { return nil, err } @@ -124,7 +105,7 @@ func (x *serviceVersionClient) Recv() (*StringResponse, error) { } func (c *serviceClient) Status(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_StatusClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[2], Service_Status_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[2], "/service/Status", opts...) if err != nil { return nil, err } @@ -156,7 +137,7 @@ func (x *serviceStatusClient) Recv() (*StringResponse, error) { } func (c *serviceClient) Stop(ctx context.Context, in *StopRequest, opts ...grpc.CallOption) (Service_StopClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[3], Service_Stop_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[3], "/service/Stop", opts...) if err != nil { return nil, err } @@ -188,7 +169,7 @@ func (x *serviceStopClient) Recv() (*Empty, error) { } func (c *serviceClient) UpdatePolicy(ctx context.Context, in *UpdatePolicyRequest, opts ...grpc.CallOption) (Service_UpdatePolicyClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[4], Service_UpdatePolicy_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[4], "/service/UpdatePolicy", opts...) if err != nil { return nil, err } @@ -220,7 +201,7 @@ func (x *serviceUpdatePolicyClient) Recv() (*Empty, error) { } func (c *serviceClient) DumpPolicies(ctx context.Context, in *DumpPoliciesRequest, opts ...grpc.CallOption) (Service_DumpPoliciesClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[5], Service_DumpPolicies_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[5], "/service/DumpPolicies", opts...) if err != nil { return nil, err } @@ -252,7 +233,7 @@ func (x *serviceDumpPoliciesClient) Recv() (*StringResponse, error) { } func (c *serviceClient) DumpPoliciesDefinitions(ctx context.Context, in *DumpPolicyDefinitionsRequest, opts ...grpc.CallOption) (Service_DumpPoliciesDefinitionsClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[6], Service_DumpPoliciesDefinitions_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[6], "/service/DumpPoliciesDefinitions", opts...) if err != nil { return nil, err } @@ -284,7 +265,7 @@ func (x *serviceDumpPoliciesDefinitionsClient) Recv() (*DumpPolicyDefinitionsRes } func (c *serviceClient) GetDoc(ctx context.Context, in *GetDocRequest, opts ...grpc.CallOption) (Service_GetDocClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[7], Service_GetDoc_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[7], "/service/GetDoc", opts...) if err != nil { return nil, err } @@ -315,8 +296,8 @@ func (x *serviceGetDocClient) Recv() (*StringResponse, error) { return m, nil } -func (c *serviceClient) ListDoc(ctx context.Context, in *ListDocRequest, opts ...grpc.CallOption) (Service_ListDocClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[8], Service_ListDoc_FullMethodName, opts...) +func (c *serviceClient) ListDoc(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_ListDocClient, error) { + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[8], "/service/ListDoc", opts...) if err != nil { return nil, err } @@ -331,7 +312,7 @@ func (c *serviceClient) ListDoc(ctx context.Context, in *ListDocRequest, opts .. } type Service_ListDocClient interface { - Recv() (*StringResponse, error) + Recv() (*ListDocReponse, error) grpc.ClientStream } @@ -339,8 +320,8 @@ type serviceListDocClient struct { grpc.ClientStream } -func (x *serviceListDocClient) Recv() (*StringResponse, error) { - m := new(StringResponse) +func (x *serviceListDocClient) Recv() (*ListDocReponse, error) { + m := new(ListDocReponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } @@ -348,7 +329,7 @@ func (x *serviceListDocClient) Recv() (*StringResponse, error) { } func (c *serviceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (Service_ListUsersClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[9], Service_ListUsers_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[9], "/service/ListUsers", opts...) if err != nil { return nil, err } @@ -380,7 +361,7 @@ func (x *serviceListUsersClient) Recv() (*StringResponse, error) { } func (c *serviceClient) GPOListScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_GPOListScriptClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[10], Service_GPOListScript_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[10], "/service/GPOListScript", opts...) if err != nil { return nil, err } @@ -412,7 +393,7 @@ func (x *serviceGPOListScriptClient) Recv() (*StringResponse, error) { } func (c *serviceClient) CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) { - stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[11], Service_CertAutoEnrollScript_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[11], "/service/CertAutoEnrollScript", opts...) if err != nil { return nil, err } @@ -455,7 +436,7 @@ type ServiceServer interface { DumpPolicies(*DumpPoliciesRequest, Service_DumpPoliciesServer) error DumpPoliciesDefinitions(*DumpPolicyDefinitionsRequest, Service_DumpPoliciesDefinitionsServer) error GetDoc(*GetDocRequest, Service_GetDocServer) error - ListDoc(*ListDocRequest, Service_ListDocServer) error + ListDoc(*Empty, Service_ListDocServer) error ListUsers(*ListUsersRequest, Service_ListUsersServer) error GPOListScript(*Empty, Service_GPOListScriptServer) error CertAutoEnrollScript(*Empty, Service_CertAutoEnrollScriptServer) error @@ -490,7 +471,7 @@ func (UnimplementedServiceServer) DumpPoliciesDefinitions(*DumpPolicyDefinitions func (UnimplementedServiceServer) GetDoc(*GetDocRequest, Service_GetDocServer) error { return status.Errorf(codes.Unimplemented, "method GetDoc not implemented") } -func (UnimplementedServiceServer) ListDoc(*ListDocRequest, Service_ListDocServer) error { +func (UnimplementedServiceServer) ListDoc(*Empty, Service_ListDocServer) error { return status.Errorf(codes.Unimplemented, "method ListDoc not implemented") } func (UnimplementedServiceServer) ListUsers(*ListUsersRequest, Service_ListUsersServer) error { @@ -684,7 +665,7 @@ func (x *serviceGetDocServer) Send(m *StringResponse) error { } func _Service_ListDoc_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(ListDocRequest) + m := new(Empty) if err := stream.RecvMsg(m); err != nil { return err } @@ -692,7 +673,7 @@ func _Service_ListDoc_Handler(srv interface{}, stream grpc.ServerStream) error { } type Service_ListDocServer interface { - Send(*StringResponse) error + Send(*ListDocReponse) error grpc.ServerStream } @@ -700,7 +681,7 @@ type serviceListDocServer struct { grpc.ServerStream } -func (x *serviceListDocServer) Send(m *StringResponse) error { +func (x *serviceListDocServer) Send(m *ListDocReponse) error { return x.ServerStream.SendMsg(m) } diff --git a/cmd/adsysd/client/doc.go b/cmd/adsysd/client/doc.go index caee5fb56..ad858e1de 100644 --- a/cmd/adsysd/client/doc.go +++ b/cmd/adsysd/client/doc.go @@ -1,25 +1,18 @@ package client import ( - "errors" + "context" "fmt" - "os" - "path/filepath" - "strings" "github.com/charmbracelet/glamour" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" "github.com/leonelquinteros/gotext" "github.com/spf13/cobra" "github.com/ubuntu/adsys" - "github.com/ubuntu/adsys/doc" "github.com/ubuntu/adsys/internal/adsysservice" + log "github.com/ubuntu/adsys/internal/grpc/logstreamer" ) func (a *App) installDoc() { - var format, dest *string docCmd := &cobra.Command{ Use: "doc [CHAPTER]", Short: gotext.Get("Documentation"), @@ -33,53 +26,37 @@ func (a *App) installDoc() { return nil, cobra.ShellCompDirectiveNoFileComp } defer client.Close() - stream, err := client.ListDoc(a.ctx, &adsys.ListDocRequest{Raw: true}) + stream, err := client.ListDoc(a.ctx, &adsys.Empty{}) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } - list, err := singleMsg(stream) + r, err := stream.Recv() if err != nil { + log.Warningf(context.Background(), "could not receive shell completion message: %v", err) return nil, cobra.ShellCompDirectiveNoFileComp } - - return strings.Split(list, "\n"), cobra.ShellCompDirectiveNoFileComp + return r.GetChapters(), cobra.ShellCompDirectiveNoFileComp }, RunE: func(cmd *cobra.Command, args []string) error { var chapter string if len(args) > 0 { chapter = args[0] } - return a.getDocumentation(chapter, *format, *dest) + return a.getDocumentation(chapter) }, } - format = docCmd.Flags().StringP("format", "f", "markdown", gotext.Get("Format type (markdown, raw or html).")) - dest = docCmd.Flags().StringP("dest", "d", "", gotext.Get("Write documentation file(s) to this directory.")) a.rootCmd.AddCommand(docCmd) } -func (a *App) getDocumentation(chapter, format, dest string) error { - if format != "markdown" && format != "raw" && format != "html" { - return fmt.Errorf("format can only be markdown, raw or html. Got %q", format) - } - +func (a *App) getDocumentation(chapter string) error { client, err := adsysservice.NewClient(a.config.Socket, a.getTimeout()) if err != nil { return err } defer client.Close() - var stream recver - var withHeader bool - if dest != "" { - stream, err = client.GetDoc(a.ctx, &adsys.GetDocRequest{Chapter: chapter}) - withHeader = true - } else if chapter == "" { - stream, err = client.ListDoc(a.ctx, &adsys.ListDocRequest{Raw: false}) - } else { - stream, err = client.GetDoc(a.ctx, &adsys.GetDocRequest{Chapter: chapter}) - withHeader = true - } + stream, err := client.GetDoc(a.ctx, &adsys.GetDocRequest{Chapter: chapter}) if err != nil { return err } @@ -89,56 +66,17 @@ func (a *App) getDocumentation(chapter, format, dest string) error { return err } - for _, out := range strings.Split(content, doc.SplitFilesToken) { - if len(out) == 0 { - continue - } - var filename string - if withHeader { - d := strings.SplitN(out, "\n", 2) - filename, out = d[0], d[1] - } - - var ext string - switch format { - case "markdown": - // Transform stdout content - if dest == "" { - r, err := glamour.NewTermRenderer(glamour.WithEnvironmentConfig()) - if err != nil { - return err - } - out, err = r.Render(out) - if err != nil { - return err - } - } - ext = ".md" - case "html": - extensions := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(extensions) - htmlFlags := html.CommonFlags | html.HrefTargetBlank | html.TOC | html.CompletePage - opts := html.RendererOptions{Flags: htmlFlags} - renderer := html.NewRenderer(opts) - out = string(markdown.ToHTML([]byte(out), parser, renderer)) - ext = ".html" - default: - } - - // Write directly on stdout - if dest == "" { - fmt.Print(out) - continue - } - - // Dump documentation in a directory - if err = os.MkdirAll(dest, 0750); err != nil { - return errors.New(gotext.Get("can't create %q", dest)) - } - if err := os.WriteFile(filepath.Join(dest, filename+ext), []byte(out), 0600); err != nil { - return errors.New(gotext.Get("can't write documentation chapter %q: %v", filename+ext, err)) - } + // Transform stdout content + r, err := glamour.NewTermRenderer(glamour.WithEnvironmentConfig()) + if err != nil { + return err } + out, err := r.Render(content) + if err != nil { + return err + } + + fmt.Print(out) return nil } diff --git a/internal/adsysservice/doc.go b/internal/adsysservice/doc.go index 5a4b3850f..13b23638b 100644 --- a/internal/adsysservice/doc.go +++ b/internal/adsysservice/doc.go @@ -4,12 +4,12 @@ import ( "bufio" "embed" "fmt" - "io" - "sort" + "path/filepath" + "regexp" "strings" "github.com/ubuntu/adsys" - "github.com/ubuntu/adsys/doc" + "github.com/ubuntu/adsys/docs" "github.com/ubuntu/adsys/internal/authorizer" log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" @@ -17,7 +17,6 @@ import ( ) // GetDoc returns a chapter documentation from server -// If chapter is empty, all documentation documentation is outputted, with a separator between them. func (s *Service) GetDoc(r *adsys.GetDocRequest, stream adsys.Service_GetDocServer) (err error) { defer decorate.OnError(&err, i18n.G("error while getting documentation")) @@ -25,50 +24,26 @@ func (s *Service) GetDoc(r *adsys.GetDocRequest, stream adsys.Service_GetDocServ return err } - onlineDocURL := doc.GetPackageURL() + onlineDocURL := docs.RTDRootURL - var out string - docDir := doc.Dir - // Get all documentation, separate file names with special characters - if chapter := r.GetChapter(); chapter == "" { - fs, err := docDir.ReadDir(".") - if err != nil { - return fmt.Errorf(i18n.G("could not list documentation directory: %v"), err) - } - // Sort all file names, having a chapter prefix - var names []string - for _, f := range fs { - names = append(names, f.Name()) - } - sort.Strings(names) + // Get all documentation metadata + _, chaptersToFiles, filesToTitle, err := docStructure(docs.Dir, "index.md", "") + if err != nil { + return fmt.Errorf(i18n.G("could not list documentation directory: %v"), err) + } - for _, n := range names { - d, err := docDir.ReadFile(n) - if err != nil { - return err - } - out = fmt.Sprintf("%s%s%s\n%s", out, doc.SplitFilesToken, strings.TrimSuffix(n, ".md"), string(d)) - } - } else { - // Get a give chapter content - filename, err := documentChapterToFileName(docDir, chapter) - if err != nil { - return err - } + // Find a match, removing trailing / for directory folder. + chapter := strings.TrimSuffix(strings.ToLower(r.GetChapter()), "/") - f, err := docDir.Open(filename) - if err != nil { - return fmt.Errorf(i18n.G("no chapter %q found in documentation"), chapter) - } - defer f.Close() + p, ok := chaptersToFiles[chapter] + if !ok { + return fmt.Errorf("no documentation found for %q", r.GetChapter()) + } - content, err := io.ReadAll(f) - if err != nil { - return fmt.Errorf(i18n.G("could not read chapter %q: %v"), chapter, err) - } - out = fmt.Sprintf("%s%s\n%s", doc.SplitFilesToken, strings.TrimSuffix(filename, ".md"), string(content)) + out, err := renderDocumentationPage(p, filesToTitle, onlineDocURL) + if err != nil { + return fmt.Errorf(i18n.G("could not read chapter %q: %v"), chapter, err) } - out = strings.ReplaceAll(out, "(images/", fmt.Sprintf("(%s/images/", onlineDocURL)) if err := stream.Send(&adsys.StringResponse{ Msg: out, @@ -79,90 +54,243 @@ func (s *Service) GetDoc(r *adsys.GetDocRequest, stream adsys.Service_GetDocServ } // ListDoc returns a list of all documentation from server. -func (s *Service) ListDoc(r *adsys.ListDocRequest, stream adsys.Service_ListDocServer) (err error) { +func (s *Service) ListDoc(_ *adsys.Empty, stream adsys.Service_ListDocServer) (err error) { defer decorate.OnError(&err, i18n.G("error while listing documentation")) if err := s.authorizer.IsAllowedFromContext(stream.Context(), authorizer.ActionAlwaysAllowed); err != nil { return err } - fs, err := doc.Dir.ReadDir(".") + chapters, _, _, err := docStructure(docs.Dir, "index.md", "") if err != nil { return fmt.Errorf(i18n.G("could not list documentation directory: %v"), err) } - // Sort all file names while they have their prefix - var names []string - for _, f := range fs { - names = append(names, f.Name()) + if err := stream.Send(&adsys.ListDocReponse{ + Chapters: chapters, + }); err != nil { + log.Warningf(stream.Context(), "couldn't send documentation to client: %v", err) } - sort.Strings(names) + return nil +} + +// docStructure parses the toc and order documentation based on subsections toc appearance. +// It returns a list of ordered chapters names for completion, a map from chapter name + alias to filename +// and filename to their title name. +// We assume toc trees are only in index files. +func docStructure(dir embed.FS, indexFilePath, parentChapterName string) (orderedChapters []string, chaptersToFiles map[string]string, filesToTitle map[string]string, err error) { + chaptersToFiles, filesToTitle = make(map[string]string), make(map[string]string) - var content string - if !r.GetRaw() { - content += "# Table of content\n" + f, err := dir.Open(indexFilePath) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not get main index file: %v", err) } - for _, n := range names { - if r.GetRaw() { - content += fmt.Sprintln(fileNameToDocumentChapter(n)) + defer f.Close() + + startToctree := "```{toctree}" + endToctree := "```" + + root := filepath.Dir(indexFilePath) + + var inTocTree bool + s := bufio.NewScanner(f) + for s.Scan() { + t := strings.TrimSpace(s.Text()) + if t == startToctree { + inTocTree = true + continue + } else if t == endToctree { + inTocTree = false + } + + // Add the original index + if strings.HasPrefix(t, "# ") && indexFilePath == "index.md" { + chaptersToFiles[""] = indexFilePath + title := strings.TrimPrefix(t, "# ") + filesToTitle[indexFilePath] = title + chapterName := toCmdlineChapterName(title, "") + chaptersToFiles[chapterName] = indexFilePath + orderedChapters = append(orderedChapters, chapterName) + } + + if !inTocTree { + continue + } + + if strings.HasPrefix(t, ":") || t == "" { continue } - title := i18n.G("- can't read content -") - if f, err := doc.Dir.Open(n); err == nil { - if title, err = bufio.NewReader(f).ReadString('\n'); err == nil { - title = strings.TrimPrefix(title, "# ") + + // We have 3 forms of content: + // page -> we need to parse page.md to get its title name + // foo/index -> we need to parse foo/index.md to get its title name and parse its tocs + // alias -> we open bar to get its title name for the reverse lookup, but also adds the alias. + alias, p, found := strings.Cut(t, "<") + p = filepath.Join(root, p) + if found { + p = strings.TrimSuffix(p, ">") + } else { + p = alias + } + + p = p + ".md" + title, err := titleFromPage(dir, p) + if err != nil { + return nil, nil, nil, err + } + filesToTitle[p] = title + + chapterName := toCmdlineChapterName(title, parentChapterName) + chaptersToFiles[chapterName] = p + // chapterName is either the page title or the alias if set. + if found { + alias = toCmdlineChapterName(alias, parentChapterName) + chaptersToFiles[alias] = p + chapterName = alias + } + orderedChapters = append(orderedChapters, chapterName) + + // Look for any children index files and merge them into the parent one. + if strings.HasSuffix(p, "index.md") { + orderedChaptersChild, chaptersToFilesChild, filesToTitleChild, err := docStructure(dir, p, chapterName) + if err != nil { + return nil, nil, nil, err } - if err = f.Close(); err != nil { - log.Infof(stream.Context(), "Can't close documentation file: %v", err) + + orderedChapters = append(orderedChapters, orderedChaptersChild...) + for chapter, p := range chaptersToFilesChild { + chaptersToFiles[chapter] = p + } + for p, title := range filesToTitleChild { + filesToTitle[p] = title } } - content += fmt.Sprintf(" 1. [**%s**] %s\n", fileNameToDocumentChapter(n), title) } - if err := stream.Send(&adsys.StringResponse{ - Msg: content, - }); err != nil { - log.Warningf(stream.Context(), "couldn't send documentation to client: %v", err) + if s.Err() != nil { + return nil, nil, nil, fmt.Errorf("can't scan index file: %v", err) } - return nil + + return orderedChapters, chaptersToFiles, filesToTitle, err +} + +// titleFromPage extracts the title from a given markdown file. +func titleFromPage(dir embed.FS, path string) (string, error) { + f, err := dir.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + s := bufio.NewScanner(f) + if !s.Scan() { + return "", fmt.Errorf("empty page") + } + + title := strings.TrimPrefix(strings.TrimSpace(s.Text()), "# ") + + if s.Err() != nil { + return "", fmt.Errorf("can't scan file: %v", s.Err()) + } + + return title, nil } -// fileNameToDocumentChapter strips prefix (before first dash) and suffix of documentation files. -func fileNameToDocumentChapter(name string) string { - parts := strings.SplitN(name, "-", 2) - if len(parts) > 1 { - name = parts[1] +// toCmdlineChapterName returns lowercase chapter name used for shell completion and user entry. +func toCmdlineChapterName(t, parentChapterName string) string { + if parentChapterName != "" { + t = filepath.Join(parentChapterName, t) } - return strings.TrimSuffix(name, ".md") + return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(t), " ", "-")) } -// documentChapterToFileName returns the first file matching the name of a chapter. -func documentChapterToFileName(dir embed.FS, chapter string) (string, error) { - fs, err := dir.ReadDir(".") +func renderDocumentationPage(p string, filesToTitle map[string]string, onlineDocURL string) (string, error) { + var out strings.Builder + + currentDir := filepath.Dir(p) + + f, err := docs.Dir.Open(p) if err != nil { - return "", fmt.Errorf(i18n.G("could not list documentation directory: %v"), err) + return "", fmt.Errorf(i18n.G("no file %q found in documentation"), p) } + defer f.Close() - // Sort all file names while they have their prefix - var names []string - for _, f := range fs { - names = append(names, f.Name()) - } - sort.Strings(names) + var inTocTree, shouldRenderTocTree bool + startToctree := "```{toctree}" + endToctree := "```" - for _, n := range names { - if strings.EqualFold(fileNameToDocumentChapter(n), chapter) { - return n, nil + s := bufio.NewScanner(f) + for s.Scan() { + l := s.Text() + trimmedLine := strings.TrimSpace(l) + if trimmedLine == startToctree { + shouldRenderTocTree = true + inTocTree = true + continue + } else if trimmedLine == endToctree { + inTocTree = false + continue + } + + // Handle to tree content by replacing visible ones with a list of titles or aliases + if inTocTree { + if trimmedLine == ":hidden:" { + shouldRenderTocTree = false + } + if strings.HasPrefix(trimmedLine, ":") || !shouldRenderTocTree || trimmedLine == "" { + continue + } + + title, _, aliasFound := strings.Cut(trimmedLine, "<") + if !aliasFound { + p = filepath.Join(currentDir, title) + ".md" + title = filesToTitle[p] + } + + l = fmt.Sprintf(" * %s", title) + } + + // Strip grids + if strings.HasPrefix(l, "````") || strings.HasPrefix(l, "```{grid") { + continue + } + /* + ### [Explanation](explanation/index) -> ### Explanation + */ + // Replace image urls + if strings.HasPrefix(l, "![") { + l = imageURLToRTDURL(l) + } + + // Remove all remaining internal links for now as we can't bind them. + l = stripInternalLinks(l) + + _, err := out.Write([]byte(l + "\n")) + if err != nil { + return "", err } } - // Try exact match, posfixing with .md if not found - if !strings.HasSuffix(chapter, ".md") { - chapter = fmt.Sprintf("%s.md", chapter) + if s.Err() != nil { + return "", fmt.Errorf("can't scan file: %v", s.Err()) } - if _, err := dir.Open(chapter); err == nil { - return chapter, nil + + return out.String(), nil +} + +var reURL = regexp.MustCompile(`\.\./images/(.+/)*`) + +func imageURLToRTDURL(imageLine string) string { + return reURL.ReplaceAllString(imageLine, docs.RTDRootURL+"/_images/") +} + +// Regular expression pattern to match [title](URL) +var reInternalLinksURL = regexp.MustCompile(`\[(.*?)\]\(.*?\)`) + +func stripInternalLinks(line string) string { + if strings.Contains(line, "http") { + return line } - return "", fmt.Errorf(i18n.G("no file found matching chapter %q"), chapter) + return reInternalLinksURL.ReplaceAllString(line, "$1") }